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 Liste de mnémoniques 0 12821 772809 771273 2026-09-22T07:51:24Z CentreMersenne 124596 /* Ordre des planètes du Système solaire */ 772809 wikitext text/x-wiki Cette page contient une '''liste de [[w:mnémotechnique|mnémotechniques]]''', c’est-à-dire différentes constructions qui facilitent la mémorisation. Par exemple : afin de retenir beaucoup plus facilement les sept péchés capitaux, une phrase mnémotechnique possible offrant une image plus visuelle regroupant une partie ou la totalité d'un péché : Par goût, Colette envie l'orgue luxueux d'Avarice (Paresse, gourmandise, colère, envie, orgueil, luxure, avarice) == Atmosphère == === Structure verticale === L’atmosphère terrestre présente une structure verticale en couches basée sur l’évolution de la température. On distingue la Troposphère (siège des phénomènes météorologiques), la Stratosphère, la Mésosphère et la Thermosphère (et l'exosphère). '''T'''out '''S'''ur '''M'''a '''T'''ête '''Trop''' de '''Str'''ess et de '''Més'''aventures '''Ter'''rifient à l''''Ex'''trême == Mathématiques == === Les 126 premières décimales du nombre π (pi) === Le nombre de lettres de chaque mot de ce poème correspond à une décimale de [[w:Pi|Pi]]. Un mot de dix lettres correspond au chiffre 0. {| |- align="center" | ''Que'' || || ''j'' || ''<nowiki>’</nowiki>'' || ''aime'' || ''à'' || ''faire'' || ''apprendre'' || ''un'' || ''nombre'' || ''utile'' || ''aux'' || ''sages'' || ''!'' |- align="center" ! 3 !! , !! 1 !! !! 4 !! 1 !! 5 !! 9 !! 2 !! 6 !! 5 !! 3 !! 5 |} {| |- align="center" | ''Immortel'' || ''Archimède'' || '','' || ''artiste'' || ''ingénieur'' || '','' |- align="center" ! 8 !! 9 !! !! 7 !! 9 |} {| |- align="center" | ''Qui'' || ''de'' || ''ton'' || ''jugement'' || ''peut'' || ''priser'' || ''la'' || ''valeur'' || ''?'' |- align="center" ! 3 !! 2 !! 3 !! 8 !! 4 !! 6 !! 2 !! 6 |} {| |- align="center" | ''Pour'' || ''moi'' || '','' || ''ton'' || ''problème'' || ''eut'' || ''de'' || ''pareils'' || ''avantages'' || ''...'' |- align="center" ! 4 !! 3 !! !!3!! 8 !! 3 !! 2 !! 7 !! 9 |} {| |- align="center" | ''Jadis'' || '','' || ''mystérieux'' || , || ''un'' || ''problème'' || ''bloquait'' |- align="center" ! 5 !! !! 0 !! !! 2 !! 8 !! 8 |} {| |- align="center" | ''Tout'' || ''l'' || ''<nowiki>’</nowiki>'' || ''admirable'' || ''procédé'' || '','' || ''l'' || ''<nowiki>’</nowiki>'' || ''œuvre'' || ''grandiose'' |- align="center" ! 4 !! 1 !! !! 9 !! 7 !! !! 1 !! !! 6 !! 9 |} {| |- align="center" | ''Que'' || ''Pythagore'' || ''découvrit'' || ''aux'' || ''anciens'' || ''Grecs'' ||''.'' |- align="center" ! 3 !! 9 !! 9 !! 3 !! 7 !! 5 |} {| |- align="center" | ''Ô'' || ''quadrature'' || ''!'' || ''Vieux'' || ''tourment'' || ''du'' || ''philosophe'' || ''...'' |- align="center" ! 1 !! 0 !! !! 5 !! 8 !! 2 !! 0 |} {| |- align="center" | ''Insoluble'' || ''rondeur'' || '','' || ''trop'' || ''longtemps'' || ''vous'' || ''avez'' |- align="center" ! 9 !! 7 !! !! 4 !! 9 !! 4 !! 4 |} {| |- align="center" | ''Défié'' || ''Pythagore'' || ''et'' || ''ses'' || ''imitateurs'' || ''.'' |- align="center" ! 5 !! 9 !! 2 !! 3 !! 0 |} {| |- align="center" | ''Comment'' || ''intégrer'' || ''l'' || ''<nowiki>’</nowiki>'' || ''espace'' || ''plan'' || ''circulaire'' || ''?'' |- align="center" ! 7 !! 8 !! 1 !! !! 6 !! 4 !! 0 |} {| |- align="center" | ''Former'' || ''un'' || ''triangle'' || ''auquel'' || ''il'' || ''équivaudra'' || ''?'' |- align="center" ! 6 !! 2 !! 8 !! 6 !! 2 !! 0 |} {| |- align="center" | ''Nouvelle'' || ''invention'' || '':'' || ''Archimède'' || ''inscrira'' |- align="center" ! 8 !! 9 !! !! 9 !! 8 |} {| |- align="center" | ''Dedans'' || ''un'' || ''hexagone'' || '';'' || ''appréciera'' || ''son'' || ''aire'' |- align="center" ! 6 !! 2 !! 8 !! !! 0 !! 3 !! 4 |} {| |- align="center" |''Fonction'' || ''du'' || ''rayon'' || ''.'' || ''Pas'' || ''trop'' || ''ne'' || ''s'' || ''<nowiki>’</nowiki>'' || ''y'' || ''tiendra'' |- align="center" ! 8 !! 2 !! 5 !! !! 3 !! 4 !!2 !! 1 !! !! 1 !! 7 |} {| |- align="center" |''Dédoublera'' || ''chaque'' || ''élément'' || ''antérieur'' |- align="center" ! 0 !! 6 !! 7 !! 9 |} {| |- align="center" |''Toujours'' || ''de'' || ''l'' || ''<nowiki>’</nowiki>'' || ''orbe'' || ''calculé'''e'''''¹ || ''approchera'' |- align="center" ! 8 !! 2 !! 1 !! !! 4 !! 8 !! 0 |} {| |- align="center" |''Définira'' || ''limite'' || '';'' || ''enfin'' || '','' || ''l'' || ''<nowiki>’</nowiki>'' || ''arc'' || '','' || ''le'' || ''limiteur'' |- align="center" ! 8 !! 6 !! !! 5 !! !! 1 !! !! 3 !! !! 2 !! 8 |} {| |- align="center" |''De'' || ''cet'' || ''inquiétant'' || ''cercle'' || '','' || ''ennemi'' || ''trop'' || ''rebelle'' |- align="center" ! 2 !! 3 !! 0 !! 6 !! !! 6 !! 4 !! 7 |} {| |- align="center" |''Professeur'', || ''enseignez'' || ''son'' || ''problème'' || ''avec'' || ''zèle'' || ''!'' |- align="center" ! 0 !! 9 !! 3 !! 8 !! 4 !! 4 |} ¹ Le mot orbe est du masculin mais ce ne fut pas toujours le cas, ceci induit à présent une faute d’accord à « ''calculée'' » que l’on peut remplacer par « ''escompté'' » pour conserver le bon nombre de lettres. === Premières décimales de l’inverse du nombre π : 1/π === La valeur de '''1/π = 0,3183098''' se retient sous la forme d’une phrase historique faisant référence aux trois glorieuses : {{Citation|« Les 3 journées de 1830 ont renversé 89 [la Révolution de 1789] »}}. Le score de la finale de la coupe du monde de football 98 a fait '''un surpris''' (1 sur pi), côté Brésil : '''0''', '''3''' (c’est le feu ! = '''18''' = '''''Cher''''' payé ?) ; '''3-0''' ('''''Gard'''''ez en souvenir) '''98''' === Trigonométrie === Le principe est de ne retenir que la première lettre ou la première syllabe des mots-clés de chaque définition ou théorème : ==== Définitions ==== * « Cosinus = côté Adjacent sur l'Hypoténuse » * « Sinus = côté Opposé sur l'Hypoténuse » * « Tangente = côté Opposé sur côté Adjacent » Une "phrase" permet de se rappeler ces trois définitions à la fois : '''cah soh toa''' pour « ''casse-toi'' » : '''C'''osinus = '''A'''djacent sur '''H'''ypoténuse ; '''S'''inus = '''O'''pposé sur '''H'''ypoténuse ; '''T'''angente = '''O'''pposé sur '''A'''djacent. Certains préfèrent '''soh cah toa'''. On peut aussi ressortir les dénominateurs de chaque fraction (afin de ne pas mélanger numérateurs et dénominateurs dans ces égalités) en apprenant les sons : '''SO-CA-TO, H-H-A''' (HHA étant les dénominateurs : '''S'''in ='''O'''pp /''H''yp , '''C'''os = '''A'''dj/''H''yp, '''T'''an='''O'''pp/''A''dj) <br /> D'autres méthodes consistent à associer un "mot" facile à retenir à chacune des trois définitions:<br /> - Cosadi - Sinopi - Tanopad <br /> - cosadjip - sinopip - tangopaj <br /> - CAHier - SOHo - TOAst (ou COCA) ==== Théorèmes ==== * « sin (a+b) = sin a cos b + cos a sin b » devient « ''sico cosi'' » * « cos (a+b) = cos a cos b - sin a sin b » devient « ''coco sisi'' » (ou « ''coco MOINS sisi'' ou « ''coco ISsi'' » pour retenir le signe) * À noter que la formule « ''sico cosi / coco moins sisi'' » ou « ''Coco si méchant, si, Coco, si'' » permet également d’apprendre les formules de factorisation suivantes : sin p + sin q = 2 sin [(p+q)/2] •cos [(p-q)/2] sin p - sin q = 2 cos [(p+q)/2] •sin [(p-q)/2] cos p + cos q = 2 cos [(p+q)/2] •cos [(p-q)/2] cos p - cos q = -2 sin [(p+q)/2] •sin [(p-q)/2] Avec p = A + B et q = A - B '''Cosinus est menteur et raciste''' ('''CO'''s comme '''CO'''n) en effet cos (a+b) donne (cos a cos b) - (sin a sin b). Cosinus est donc menteur puisque le signe de l’addition (positive) est négatif. Cosinus est raciste puisque on obtient (cos a cos b) d’une part et (sin a sin b) d’autre part : les cosinus et les sinus ne se mélangent pas. '''co'''sinus est un '''co'''pain '''co'''n (copain pour le sens et con pour le signe): cos(a'''+'''b)= cos(a)cos(b) '''-''' sin(a)sin(b) : les cosinus restent ensemble, mais le signe change. '''s'''inus est une '''s'''alade '''s'''ympa (salade pour le sens et sympa pour le signe): sin (a'''+'''b)=sin(a)cos(b) '''+''' sin(b)cos(a) : sin et cos se mélangent mais le signe reste le même On trouve également : '''opip adjip opadj''' : sinus ('''op'''posé sur '''hyp'''oténuse), cosinus ('''adj'''acent sur '''hyp'''oténuse), tangente ('''op'''posé sur '''adj'''acent). La phrase prononcée rapidement d’un seul coup est très facile à mémoriser. De même que '''SOH CAH TOA''': '''S'''inus= '''O'''pposé sur '''H'''ypoténuse '''C'''osinus= '''A'''djacent sur '''H'''ypoténuse '''T'''angente= '''O'''pposé sur '''A'''djacent On peut lui substituer la formule plus percutante : '''CAH SOH TOA''' (à prononcer Casse toi ! ) === Dates et constantes === Le [[w:code chiffres-sons|code chiffres-sons]] est une méthode qui permet de se souvenir de dates ou de valeurs numériques en formant des phrases. === Formules de géométrie === * Circonférence d’un cercle : 2 pi R (2 pierres) ** La circonférence est toute fière d’être égale à 2 pi R * Aire d’un disque: pi R<sup>2</sup> (« pierre carrée » ou « pierre deux ») ** Le cercle est tout joyeux d’être égal à pi R<sup>2</sup> (prononcer « pi R deux ») * « Le volume de la sphère, est quoi qu’on y puisse faire, 4/3 pi R<sup>3</sup>, fut-elle de bois. » ([[w:fr:Marcel Pagnol|Marcel Pagnol]]) Le volume d'une sphère, qu'elle soit de pierre, qu'elle soit de bois est égal aux 4/3 de pi R3 * Le volume d'une pizza (d'un camembert, ou de n'importe quel objet semblable) de rayon 'z' et de hauteur 'a' est égale à '''Pi.(z.z).a''' (la formule correspond à son nom) soit V = π.z<sup>2</sup>.a === Analyse vectorielle === [[File:DRG chart fr.svg|thumb|right|300px|Diagramme des principales relations entre opérateurs de calcul vectoriel.]] * Opérateurs s'annulant: '''DiR'''i'''G'''é (décrivant les flèches centrales sur le diagramme à droite) ** '''DiR'''i: <math>\mathrm{div}(\overrightarrow{\mathrm{rot}})=0</math> ** '''R'''i'''G''': <math>\overrightarrow{\mathrm{rot}}(\overrightarrow{\mathrm{grad}})=\vec{0}</math> * Autres formules (flèches reliant div et grad sur le diagramme à droite): ** <math>\Delta = \mathrm{div}(\overrightarrow{\mathrm{grad}})</math> ** <math>\overrightarrow{\mathrm{grad}}(\mathrm{div})= \overrightarrow{\mathrm{rot}}(\overrightarrow{\mathrm{rot}})+\vec{\Delta}</math> === Ordre des opérations === En algèbre, les opérations simples : <code>(</code>&nbsp;<code>)</code>, <code>+</code>, <code>-</code>, <code>&times;</code> et <code>&divide;</code>, sont évaluées selon un certain ordre : '''PEMDAS''' pour « '''p'''arenthèses, '''e'''xposant, '''m'''ultiplication, '''d'''ivision, '''a'''ddition et '''s'''oustraction ». Pour plus de détails sur l'application de ce mnémonique, voir [[:w:fr:Ordre des opérations|Ordre des opérations]]. === Double distributivité === Retenir le mot « '''PIED''' » qui donne les termes à regrouper lorsque l’on développe : '''P'''remiers, '''I'''ntérieurs, '''E'''xtérieurs, '''D'''erniers. === Constante e<ref name="villemin.gerard">http://villemin.gerard.free.fr/Wwwgvmm/MnemoTe/Phrase.htm</ref> === {| border="0" cellpadding="0" cellspacing="1" |align="center"|Tu | &nbsp; |align="center"|aideras | &nbsp; |align="center"|à | &nbsp; |align="center"|rappeler | &nbsp; |align="center"|ta | &nbsp; |align="center"|quantité | &nbsp; |align="center"|à | &nbsp; |align="center"|beaucoup | &nbsp; |align="center"|de | &nbsp; |align="center"|docteurs | &nbsp; |align="center"|amis. |- |align="center"|2 | ,&nbsp; |align="center"|7 | &nbsp; |align="center"|1 | &nbsp; |align="center"|8 | &nbsp; | align="center" |2 | &nbsp; |align="center"|8 | &nbsp; |align="center"|1 | &nbsp; |align="center"|8 | &nbsp; |align="center"|2 | &nbsp; |align="center"|8 | &nbsp; |align="center"|4 |} === Nombre d'or<ref name="villemin.gerard"/> === {| border="0" cellpadding="0" cellspacing="1" |align="center"|Ô | &nbsp; |align="center"|nombre | &nbsp; |align="center"|d' | &nbsp; |align="center"|élégance | &nbsp; |align="center"|! | &nbsp; |align="center"|Toi, | &nbsp; |align="center"|toi, | &nbsp; |align="center"|grandiose, | &nbsp; |align="center"|étonnant : | &nbsp; |align="center"|''le nombre d'or''. |- |align="center"|1 | ,&nbsp; |align="center"|6 | &nbsp; |align="center"|1 | &nbsp; |align="center"|8 | &nbsp; |align="center"|0 | &nbsp; |align="center"|3 | &nbsp; |align="center"|3 | &nbsp; |align="center"|9 | &nbsp; |align="center"|8 | &nbsp; |align="center"| &nbsp; |} (! pour 0) === Statistiques === * [[w:Règle 68-95-99.7|Règle 68-95-99.7]] : la proportion des échantillons entre [-σ, +σ], [-2σ, +2σ], [-3σ, +3σ] pour une distribution gaussienne centrée. * Erreurs de première espèce et deuxième espèce. ** Se rappeler la fable d’Ésope dans laquelle un enfant [[wikt:crier au loup|crie au loup]] (hypothèse nulle <math>H_0</math>: « il n'y a aucun loup »). **# D'abord, les villageois pensent qu'il y a un loup alors qu'il n'y en a aucun (erreur de première espèce). **# Puis, les villageois pensent qu'il n'y a aucun loup alors qu'il y en a un (erreur de seconde espèce). ** Il y a une barre dans '''P'''ositif (faux positif : erreur de type '''I''') et deux barres dans '''N'''égatif (faux négatif : erreur de type '''II'''). == Sciences == === Astronomie === ==== Ordre des planètes du Système solaire ==== Il existe toute une série de termes mnémotechniques pour se souvenir de l'ordre des planètes à l’intérieur du [[w:système solaire|Système solaire]]. La première lettre de chaque mot de cette phrase correspond à la première lettre de chaque [[w:planète|planète]], de la plus rapprochée à la plus éloignée du Soleil. L'[[w:apostrophe|apostrophe]] ou la [[w:virgule|virgule]] peut représenter la [[w:ceinture d'astéroïdes|ceinture d'astéroïdes]] entre [[w:Mars (planète)|Mars]] et [[w:Jupiter|Jupiter]]. Voici l’ordre des planètes du Système solaire : Mercure, Vénus, Terre, Mars, Jupiter, Saturne, Uranus, Neptune. ''À NOTER que selon la [[w:Définition des planètes de l'UAI|nouvelle définition]] de l’[[w:Union astronomique internationale|Union astronomique internationale]] d’août [[w:2006|2006]], [[w:Pluton (planète naine)|Pluton]] n’est plus considérée comme une [[w:planète|planète]] mais comme une planète naine ([[w:(134340) Pluton|(134340) Pluton]])'' (de même que [[w:(1) Cérès|(1) Cérès]], [[w:(136199) Éris|(136199) Éris]], [[w:(136108) Haumea|(136108) Haumea]] et [[w:(136472) Makemake|(136472) Makemake]]), <br/> '''''Ordre des planètes du Système solaire : '''Mercure, Vénus, Terre, Mars, Jupiter, Saturne, Uranus, Neptune.'' On emploie par exemple les phrases suivantes : *'''''M'''ême '''V'''ieux '''T'''ruc '''M'''ais '''J''''en '''S'''ais '''U'''n '''N'''ouveau.'' *'''''M'''a '''V'''ieille '''T'''ante '''M'''arie a '''J'''eté '''S'''amedi '''U'''n '''N'''avet.'' *'''''M'''a '''V'''ieille '''T'''rompette '''M'''e '''J'''oue '''S'''on '''U'''ltime '''N'''octurne.'' *'''''M'''a '''V'''oiture '''T'''e '''M'''ène '''J'''oyeusement '''S'''ur '''U'''ne '''N'''ationale.'' *'''''M'''arie, '''V'''iendras-'''T'''u '''M'''anger '''J'''eudi '''S'''ur '''U'''ne '''N'''appe ?'' *'''''M'''angez '''V'''os '''T'''artes, '''M'''ais '''J'''uste '''S'''ur '''U'''ne '''N'''appe .'' *'''''M'''e '''V'''oici '''T'''oute '''M'''ignonne''',''' '''J'''e '''S'''uis '''U'''ne '''N'''ébuleuse.'' *'''''M'''e '''V'''oici '''T'''oute '''M'''odifiée''',''' '''J'''e '''S'''uis '''U'''ne '''N'''ouveauté.'' *'''''M'''e '''V'''oilà '''T'''out '''M'''ouillé''',''' '''J''''ai '''S'''uivi '''U'''n '''N'''uage.'' *'''''M'''e '''V'''oilà '''T'''oute '''M'''ouillée''',''' '''J'''e '''S'''uis '''U'''ne '''N'''ymphomane.'' *'''''M'''on '''V'''ieux, '''T'''u '''M''''as '''J'''eté '''S'''ur '''U'''ne '''N'''avette.'' *'''''M'''on '''V'''ioloncelle '''T'''ombe, '''M'''ais '''J'''e '''S'''auve '''U'''ne '''N'''ote. *'''''M'''aman '''V'''ole '''T'''ous '''M'''es '''J'''ouets, '''S'''auf '''U'''n '''N'''ounours !'' *'''''M'''e '''V'''oici, '''T'''onton '''M'''arcel, '''J'''e '''S'''uis '''U'''n '''N'''ageur.'' *'''''M'''a '''V'''ille '''T'''hionville '''M'''ontre '''J'''oyeusement '''S'''on '''U'''nivers '''N'''octurne.'' *'''''M'''on '''V'''élo '''T'''e '''M'''ènera ''' J'''usque '''S'''ur '''U'''n '''N'''uage.'' *'''''M'''on '''V'''élo '''T'''ourne '''M'''al, ''' J''''en '''S'''ouhaite '''U'''n '''N'''ouveau.'' *'''''M'''onsieur, '''V'''ous '''T'''ravaillez '''M'''al ; - ''' J'''e '''S'''uis '''U'''n '''N'''ovice.'' *'''''M'''al '''V'''êtu '''T'''oi '''M'''ême, '''J'''e '''S'''uis '''U'''n '''N'''udiste'' *'''''M'''on '''V'''ieux '''T'''outou '''M'''édor '''J'''oue '''S'''ur '''U'''n '''N'''uage'' *'''''M'''a '''V'''erge '''T'''e '''M'''ènera '''J'''usque '''S'''ur '''U'''n '''N'''uage'' *'''''M'''ais '''V'''ous '''T'''ombez '''M'''al, '''J''''ai '''S'''auté '''U'''ne '''N'''aine'' * '''''M'''essieurs!''' V'''otre '''T'''rahison '''M'''<nowiki/><nowiki>'écœure: </nowiki>'''J'''ouer''' S'''ur '''U'''ne '''N'''omenclature!'' (au sujet de la disparition de Pluton de la liste) *'''''S'''ors (pour Soleil) -'''M'''oi '''V'''ite '''T'''a '''M'''armite '''J'''aune '''S'''ur '''U'''ne '''N'''appe.'' *'''''M'''arquez '''V'''otre '''T'''emps '''M'''esuré '''J'''uste '''S'''ous '''U'''ne '''N'''anoseconde.'' '''Et pour les nostalgiques de Pluton...''' * '''''M'''anon '''V'''iendras '''T'''u '''M'''anger '''J'''eudi '''S'''ur '''U'''ne '''N'''appe '''P'''ropre.'' *'''''M'''ercure '''V'''eut '''T'''aquiner '''M'''ars, '''J'''e '''S'''uis '''U'''ne '''N'''ouvelle '''P'''lanète.'' *'''''M'''ademoiselle, '''V'''ous '''T'''ravaillez '''M'''al, '''J'''e '''S'''uis '''U'''n '''N'''ouveau '''P'''rofesseur'' *''Le '''M'''onde '''V'''oit '''T'''ourner du '''M'''atin '''J'''usqu'au '''S'''oir '''U'''niquement '''N'''euf '''P'''lanètes''. *'''''M'''on '''V'''ieux, '''T'''u '''M'''e '''J'''ettes '''S'''ur '''U'''ne '''N'''ouvelle '''P'''lanète.'' *'''''M'''onsieur '''V'''euillez '''T'''ournez '''M'''a '''J'''upe '''S'''ans '''U'''ne '''N'''aïve '''P'''udeur.'' *'''''M'''e '''V'''oici, '''T'''oute '''M'''ignonne, '''J'''e '''S'''uis '''U'''ne '''N'''ouvelle '''P'''lanète. *'''''M'''e '''V'''oici, '''T'''onton '''M'''arcel, '''J'''e '''S'''uis '''U'''n '''N'''ageur '''P'''rofessionnel. *'''''M'''e '''V'''oici, '''T'''out '''M'''ouillé, '''J'''e '''S'''uis '''U'''n '''N'''ageur '''P'''ressé. *'''''M'''e '''V'''oici, '''T'''out '''M'''ouillé, '''J''''ai '''S'''uivi '''U'''n '''N'''uage '''P'''luvieux. *'''''M'''ais '''V'''iendras-'''T'''u '''M'''anger, '''J'''ulie, '''S'''ur '''U'''ne '''N'''appe '''P'''ropre. *'''''M'''on '''V'''ieux '''T'''héâtre '''M'''e '''J'''oue '''S'''ouvent '''U'''ne '''N'''ouvelle '''P'''ièce. *'''''M'''on '''V'''ieux, '''T'''u '''M''''as '''J'''eté '''S'''ur '''U'''ne '''N'''ouvelle '''P'''lanète. *'''''S'''ors (pour Soleil) -'''M'''oi '''V'''ite '''T'''a '''M'''armite '''J'''aune '''S'''ur '''U'''ne '''N'''appe '''P'''ropre.'' *'''''M'''onsieur '''V'''ous '''T'''irez '''M'''al '''J'''e '''S'''uis '''U'''n '''N'''ovice '''P'''itoyable. * '''''Mé'''lanie, '''V'''ous '''T'''ombez '''Ma'''l, '''J'''e '''S'''uis '''U'''n '''N'''avet '''P'''ourri.'' *'''''M'''on '''V'''aisseau '''T'''e '''M'''ènera '''J'''eudi '''S'''ur '''U'''ne '''N'''ouvelle '''P'''lanète. *'''''M'''a '''V'''ieille '''T'''ante '''M'''arge '''J'''oue '''S'''ur '''U'''n '''N'''ouveau '''P'''iano. *'''''M'''aman '''V'''ole '''T'''ous '''M'''es '''J'''ouets, '''S'''auf '''U'''n '''N'''ounours '''P'''ourri !'' *'''''M'''arin '''V'''aleureux, '''T'''u '''M'''ourras '''J'''eune '''S'''ur '''U'''n '''N'''avire '''P'''erdu !'' *'''''M'''on '''V'''élo '''T'''ourne '''M'''al '''J'''e '''S'''uis '''U'''n '''N'''ouveau '''P'''iéton. *'''''M'''erci '''V'''ous '''T'''ous '''M'''aintenant '''J'''e '''S'''ais '''U'''nir '''N'''euf '''P'''lanètes. *'''''M'''élanie '''V'''ous '''T'''ombez '''M'''al '''J'''e '''S'''uis '''U'''n '''N'''avet '''P'''ourri. *'''''M'''es '''V'''ieilles '''T'''antes '''M'''angeaient '''J'''adis '''S'''ur '''U'''ne '''N'''appe '''P'''ercée.'' *'''''M'''ets '''V'''ite '''T'''on '''M'''aillot '''J'''e '''S'''uis '''U'''n '''N'''udiste '''P'''oilu. * '''''M'''on '''V'''ieux '''T'''acot '''M'''´a '''J'''eté '''S'''ur '''U'''n '''N'''oble '''P'''''assant. *'''''M'''ange '''V'''ite '''T'''on '''M'''ars '''J''' 'en '''S'''ors '''U'''n '''N'''ouveau '''P'''aquet. *'''''MÈR'''E, '''V'''iens '''Ter'''miner '''M'''a '''JUP'''e, '''SA''' cout'''UR'''e '''NE''' tient '''PLU'''s.'' * '''''M'''aman, '''V'''oudrais-'''T'''u '''M''''emmener '''J'''ouer '''S'''ur '''U'''ne '''N'''ouvelle '''P'''lanète ?'' * '''''M'''on '''V'''ieux '''T'''héâtre '''M'<nowiki/>'''a '''J'''oué '''S'''ouvent '''U'''ne '''N'''ouvelle '''P'''ièce'' Suite à un concours qui s’est déroulé au Québec, la formule suivante a été retenue : *'''''M'''angez '''V'''os '''T'''artes, '''M'''ais '''J'''uste '''S'''ur '''U'''ne '''N'''appe !'' Il existe aussi cette formule (la plus ancienne mnémonique connue en astronomie) qui se retient facilement, grâce à ses trois mots de trois syllabes : *''Merveter, Marjusa, Uneplu'' ('''Mer'''cure, '''Vé'''nus, '''Ter'''re, '''Mar'''s, '''Ju'''piter, '''Sa'''turne, '''U'''ranus, '''Nep'''tune, '''Plu'''ton) Une variante<ref>Formule tirée de l’un des tomes du [https://fr.m.wikipedia.org/wiki/Manuel_des_Castors_Juniors ''Manuel des Castors Juniors'']</ref> de celle-ci : * ''Mervé'', ''Termaju'', ''Saturneplu'' ('''Mer'''cure, '''Vé'''nus, '''Ter'''re, '''Ma'''rs, '''Ju'''piter, '''Sat'''urne, '''Ur'''anus, '''Ne'''ptune, '''Plu'''ton) Qui existe aussi sous cette forme : * ''Mervé, Termaju, Satur n'est plus'' Et celle-ci qui inclut le Soleil : *'''''S'''alut ! '''Me''' '''v'''ois-'''t'''u ? '''M'''oi '''j'''e '''s'''uis '''u'''ne '''n'''ouvelle '''pl'''anète !'' '''Planète ayant un système d'anneaux''' * '''J'''e '''S'''uis '''U'''ne '''N'''ouille (Jupiter, Saturne, Uranus, Neptune) ==== Ordre des quatre lunes principales de Jupiter ==== '''I'''l '''e'''st '''g'''rand, '''C'''harles ! * [[w:Io|Io]], [[w:Europe|Europe]], [[w:Ganymède_(lune)|Ganymède]], [[w:Callisto_(lune)|Callisto]] ==== Croissant de Lune ==== Le '''p'''remier croissant et le '''d'''ernier croissant peuvent être reconnus en les assimilant aux sens du p et du d. En effet, en « ajoutant » au croissant de lune un bâton, on obtient un p ou un d selon le croissant. Cette méthode marche uniquement dans l'[[w:Hémisphère (géographie)|hémisphère]] [[w:nord|nord]], dans l’hémisphère sud il faudra considérer que la Lune ment. Une méthode plus simpliste consistait autrefois à lire le croissant de lune directement. Quand il formait un '''C''' la lune incitait à penser qu'elle était '''C'''roissante . Or dans ce cas là elle est décroissante. Et quand elle formait un '''D''' (en supposant l’ajout de la barre droite nécessaire) elle incitait à penser qu’elle était '''D'''écroissante. Or dans ce cas là elle est croissante. Il en est venu l’expression populaire : ''Il est menteur comme la lune''. Cependant, dans ce cas la Lune ne ment que dans l'hémisphère Nord : C correspond bien à la Lune croissante et D à la Lune décroissante. Ces méthodes ne sont pas valables entre les tropiques, où le sens de ''lecture'' varie selon les saisons. ==== [[w:type spectral|Types spectraux]] [[w:étoile|stellaires]] ==== Les différents [[w:type spectral|types spectraux]], du plus chaud au plus froid, sont : O, B, A, F, G, K, M. '''''O'''h, '''b'''e '''a''' '''f'''ine '''g'''irl/'''g'''uy, '''k'''iss '''m'''e !'' '''''O'''verseas '''b'''roadcast: '''a''' '''f'''lash! [[w:Godzilla|'''G'''odzilla]] '''k'''ills [[w:Mothra|'''M'''othra]] !'' === Physique === ==== Électromagnétique ==== Énergie électrique stockée dans un condensateur&nbsp;: <math>E= (1/2) C U^2</math> "l'''e''' '''demi''' '''cu'''l '''carré'''" ==== Les sept unités fondamentales==== Pour: ''seconde, ampère, candela, kilogramme, mètre, kelvin, mole&nbsp;'': <br> Sac km km <br> Je <u>'''s'''ais q</u>u<u>'''a'''n</u>d <u>'''c'''a</u>c<u>'''k'''i</u> <u>'''m'''et</u> <u>'''k'''el</u> <u>'''m'''o</u>t ! : (je) ''Sec-Am-Ca-Ki-Mè-Kel-Mo'' <br> <u>Ce con</u> d'<U>Ampère</U>, <u>qu'en d</u>it <u>qui l'au</u>ra!, <u>mettr</u>a <u>quel vin</u> au <u>môl</u>e ?: ''Sec<s>onde</s>'' d' ''ampère'', ''cand<s>ela</s>'' ''kilo<s>gramme</s>''ra, ''mètr<s>e</s>''a ''kelvin'' au ''moles'' "Secondes molles, quand des lacs-îlots [[wikt:grammer|grammant]] [[wikt:pairer|pairent]], Maître Kelvin !" (qui se prononce comme : "seconde, mole, candela, kilogramme, ampère, mètre, kelvin") * ==== Ordre des couleurs de [[w:Résistance (composant)#Repérage et valeurs normalisées|résistance électrique]] ==== ('''N'''oir, '''M'''arron, '''R'''ouge, '''O'''range, '''J'''aune, '''V'''ert, '''B'''leu, '''Vio'''let, '''G'''ris, '''B'''lanc) '''''N'''e '''M'''ange '''R'''ien '''O'''u '''J'''e '''V'''ais '''B'''leuir '''V'''iolemment (ton) '''G'''ros '''B'''laze.'' '''''N'''e '''M'''angez '''R'''ien '''O'''u '''J'''e '''V'''ous '''B'''rule '''V'''otre '''G'''rosse '''B'''arbe.'' '''''N'''e '''M'''angez '''R'''ien '''O'''u '''J'''eûnez, '''V'''oilà '''B'''ien '''V'''otre '''G'''rande '''B'''êtise.'' '''''N'''e '''M'''angez '''R'''ien '''O'''u '''J'''e '''V'''ous '''B'''rise '''V'''otre '''G'''rosse '''B'''outeille.'' '''N'''e '''M'''angez '''R'''ien '''O'''u '''J'''e '''V'''ous '''B'''ats '''V'''iolemment '''G'''ros '''B'''êta '''''N'''adine '''M'''e '''R'''épondit''' O'''ui, '''J'''e '''V'''eux '''B'''ien '''V'''otre '''G'''rosse '''B'''iroute'' '''''N'''oir, '''M'''arron, les couleurs de l'arc en ciel (sauf l'indigo), '''G'''ris, '''B'''lanc.'' Version québécoise utilisant la lettre '''B''' pour Brun au lieu de Marron : '''''N'''otre '''B'''ar '''R'''estera '''O'''uvert '''J'''eudi et '''V'''endredi. '''B'''ière et '''V'''in '''G'''ratuit, '''B'''ienvenue.'' '''''N'''otre '''B'''ar '''R'''estera '''O'''uvert '''J'''eudi et '''V'''endredi. '''B'''ien'''V'''enue '''G'''ros '''B'''uveur.'' ==== Ordre des couleurs du [[w:Couleur#Le spectre lumineux|spectre visible]] ==== Les sept couleurs du spectre visible ou de l'arc-en-ciel (dans l'ordre des fréquences croissantes : '''R'''ouge - '''O'''range - '''J'''aune - '''V'''ert - '''B'''leu - '''I'''ndigo - '''V'''iolet) peuvent se retenir grâce à la phrase suivante : La '''ROU'''sse '''OR'''ienta le '''J'''uge '''VER'''s le '''BL'''azer de l''''INDI'''enne '''VIOL'''ée. Dans l'ordre inverse (soit de la plus petite à la la plus grande longueur d'onde) elles peuvent se retenir grâce au mot '''VIBUJOR''', en remplaçant le '''U''' par un '''V''' ('''vert''' comme '''Hu'''lk). '''V'''iolet - '''I'''ndigo - '''B'''leu - '''V'''ert - '''J'''aune - '''O'''range - '''R'''ouge Remarque : en se figurant le drapeau français '''bleu'''-'''blanc'''-'''rouge''', on peut retrouver l’ordre des longueurs d’onde, en assimilant le bleu à l’ultraviolet, le blanc au visible, et le rouge à l’infra-rouge : '''ultraviolet'''-'''visible'''-'''infra-rouge'''. ==== Longueur d'onde des couleurs ==== Le mot rouge est plus long que le mot bleu (5 vs 4), sa longueur d'onde est plus longue également. ==== Couleurs en peinture et rayonnements lumineux ==== Les peintres utilisent les trois couleurs fondamentales '''Cyan Magenta et Jaune''', chacune absorbant une seule des trois couleurs fondamentales de la lumière (Rouge Vert et Bleu). Notre œil ne reconnaît la couleur que par la lumière identifiée par chacune des 3 familles de cônes de l'œil respectivement sensibles aux rayonnements '''Rouge Vert et Bleu'''. Cette phrase permet aux peintres et aux physiciens d'identifier un équivalent des deux couleurs de rayonnement lumineux identifiées par les cônes de l'œil pour chacune des couleurs fondamentales de la peinture. '''C'''ette '''B'''onne '''V'''ieille<br /> '''M'''ijote des '''R'''aviolis "'''B'''uitoni"<br /> '''<nowiki>J'</nowiki>'''en '''R'''é'''V'''ais La couleur '''C'''yan de la peinture correspond ainsi à la réception des rayonnements lumineux '''B'''leu et '''V'''ert. Le '''M'''agenta, pour sa part correspond aux rayonnements lumineux '''R'''ouge et '''B'''leu. Quant à la couleur '''J'''aune, elle renvoie vers l'œil les rayonnements lumineux '''R'''ouge et '''V'''ert. ==== Constantes ==== * vitesse de la lumière<ref name="villemin.gerard" /> : :{| style="text-align: center;" |Ah,||messagère||admirable,||lumière||éclatante,||je||sais||votre||célérité|| |- |La||constante||lumineuse||restera||désormais||là||dans||votre||cervelle|| |- |2||9||9||7||9||2||4||5||8||m/s |} * définition formelle d'une seconde (périodes de la radiation correspondant à une transition entre les deux sous-niveaux hyperfins du césium 133) : :{| style="text-align: center;" |« Pharaonne,||j'||affirmais||là,||honore||mal||l'||aimable||seconde||0 ! » |- |9||1||9||2||6||3||1||7||7||0 |} === Chimie === '''Priorité des groupes caractéristiques en nomenclature''' Pour nommer une molécule composée de plusieurs groupes caractéristiques, on utilise l'ordre suivant : Acide carboxylique - anhydride d'acide - ester - halogénure d'acyle - amide - nitrile - aldéhyde - cétone - alcool - amine - alcyne - alcène - éther-oxyde - dérivé halogéné - alcane Pour se rappeler de l'ordre : '''Ac'''e '''an'''nule '''Ester''' ! '''Halo ami'''? '''Ni'''e '''al'''ors '''Cé'''cile '''alcool'''isée, '''Am'''élie '''ascene''' à N'''eoxi''' et '''De'''lp'''h'''ine un '''alcane''' ==== Radicaux alkyles ==== Pour se rappeler l’ordre des 3 premiers groupement alcanes : * Il ai'''mait''' '''êt'''re '''pro'''pre. (Oralement, "Il ai'''Mét Éth Prop''' ") Pour se rappeler l’ordre des 4 premiers groupement alcanes : * ('''M'''éthane, '''É'''thane, '''P'''ropane, '''B'''utane) * '''M'''aman '''Et''' '''P'''apa '''B'''ébé * '''M'''aman '''Et''' '''P'''apa '''But'''inent. * '''M'''alin qui '''É'''tudie '''P'''our le '''B'''ac. * '''M'''ieux '''É'''tudier '''P'''our le '''B'''ac. * '''M'''on '''É'''cole '''P'''eut '''B'''rûler. * '''M'''on '''É'''lève '''P'''isse '''B'''ien * '''M'''organe '''E'''st '''P'''as '''B'''elle. * '''Me'''s '''é'''lèves '''p'''arlent '''b'''eaucoup. * '''M'''ets '''t'''es '''Prop'''res '''But'''s ! (Oralement, "Mét Éth Prop But") Pour se rappeler l’ordre des 5 premiers groupement alcanes : * ('''M'''éthane, '''É'''thane, '''P'''ropane, '''B'''utane, '''P'''entane) * '''M'''aman '''E'''st '''P'''artie '''B'''ébé '''P'''leure * '''M'''amie '''E'''st '''P'''artie '''B'''oire une '''P'''inte Pour se rappeler l’ordre des 6 premiers groupement alcanes : * ('''M'''éthane, '''É'''thane, '''P'''ropane, '''B'''utane, '''P'''entane, '''H'''exane) * '''M'''aurice '''E'''st '''P'''as '''B'''eau '''P'''our '''H'''élène. * '''M'''amie '''E'''t '''P'''api '''B'''atifolent '''P'''endant l''''H'''iver. * '''M'''aman '''E'''t '''P'''apa '''B'''oivent '''P'''endant '''H'''alloween * '''M'''amie '''E'''st '''P'''artie '''B'''oire une '''P'''inte de '''H'''eineken * '''M'''et '''E'''th '''P'''rop '''B'''ut '''P'''ent '''H'''ex Pour les plus vulgaires : * '''M'''audite '''É'''paisse ! '''P'''ourquoi '''B'''aiser '''P'''our l' '''H'''iver ! ==== [[w:Tableau périodique des éléments|Tableau périodique des éléments]] ==== Il est à noter que la plupart des moyens mnémotechniques concernant les éléments ont été créés par des [[w:étudiant|étudiant]]s, d’où le [[w:vocabulaire|vocabulaire]] parfois amusant des maximes. ===== [[w:Éléments de la période 2|Période 2]] ===== '''Pour : Li'''thium, '''Bé'''ryllium, '''B'''ore, '''C'''arbone, '''N'''itrogène (Azote), '''O'''xygène, '''F'''luor, '''Né'''on.'' * «La '''Li''''''Bé'''llule '''B'''leue, d’une '''C'''aresse, '''N'''oit dans l’'''O'''nde la '''F'''leur de '''Né'''nuphare. » * « '''Li'''thus et '''Be'''rénice '''B'''oivent, '''C'''haque '''N'''uit, '''O''' '''F'''rais de '''Né'''ron » * « '''Li'''verpool, '''Be'''rceau des '''B'''eatles, '''C'''onnait '''N'''aturellement ces '''O'''librius '''F'''ous et '''Né'''vrosés » * « '''Li'''bérez '''Be'''n '''B'''arkans, '''C'''élèbre '''N'''arrateur, '''O'''u '''F'''usillez '''Né'''ron » * « '''Li''' '''Be''' le '''B'''on '''C'''anard du '''N'''ord '''O'''uest de la '''F'''rance '''Ne'''ogauchiste » * « '''Li'''li '''Be'''sa '''B'''ien '''C'''ouchée '''N'''ue '''O''' '''F'''lanc de '''Né'''ron » * « '''Li'''li '''Be'''se '''B'''ien '''C'''hez '''N'''otre '''O'''ncle '''F'''umeur de '''Ne'''squik » * « '''Li'''li '''Be'''se '''B'''ien '''C'''onfortablement '''N'''otre '''O'''ncle '''F'''rançois '''Ne'''stor » * « '''Li'''mace '''Be'''te '''B'''ouffa '''C'''inq '''N'''ouveaux '''O'''ignons '''F'''raîchement '''Né'''s » * « '''Li'''li '''Be'''rça '''B'''ébé '''C'''hez '''N'''otre '''O'''ncle '''F'''ernand '''N'''estor * « '''Li'''li '''B'''ecta '''B'''ien '''C'''hez '''N'''otre '''O'''ncle '''F'''erdinand '''N'''estor » * « '''Li'''li '''Bé'''cha '''B'''ien '''C'''hez '''N'''otre '''O'''ncle '''F'''rançois-(ou '''F'''erdinand-)'''Ne'''stor »<br /> * « '''Li'''li '''Bé'''se '''B'''ien '''C'''hez '''N'''otre '''O'''ncle '''F'''rançois-(ou '''F'''erdinand-)'''Ne'''stor »<br /> * « '''LiBe'''rté '''B'''afouée '''C'''ontre '''N'''otre '''O'''rganisation '''F'''édérale '''Né'''ogaulliste (ou '''Né'''otrotskiste) » * « le '''Li''' t de '''BE''' '''B''' é a '''C'''assé le '''N'''ez de l' '''O'''ncle '''F'''urieux '''Né'''on » * « '''Li'''vrez '''Bê'''tement '''B'''ataille '''C'''ar '''N'''ous, '''O'''fficiers '''F'''rançais, '''Né'''gocions » * « '''L'i'''magination '''Be'''lliqueuse '''B'''aissa '''C'''ar '''N'''otre '''O'''rdre '''F'''ut '''Ne'''t » * « '''Li'''re '''Be'''aucoup '''B'''alzac '''C'''ar '''N'''otre '''O'''rthographe '''F'''ait '''Né'''gligé » * « '''Li'''ste de '''Be'''lles '''B'''outeilles de '''C'''ognac '''N'''ous '''O'''nt '''F'''outus '''Ne'''rveux » * « '''Li'''li '''Be'''cote '''B'''ien '''C'''omme '''Ni'''cole '''O''' '''F'''ond '''N'e'''st ce pas » * « '''Li'''bérez '''Be'''rnard '''B'''ossu '''C'''ontre '''N'''ouvel '''O'''tage '''F'''éminin. Signé '''Ne'''on » * « '''LiBe'''rté de '''B'''oire '''C'''ar '''N'''ous '''O'''n '''F'''oire '''N'''os '''e'''xams » * « '''Li'''bérez '''Be'''n '''B'''arka '''C'''ar '''N'''ous, '''O'''fficiers '''F'''rançais, '''Né'''gocions » * « '''Li'''li et '''Be'''rnard '''B'''aisent '''C'''omme '''N'''ous '''O'''n '''F'''ait '''Ne'''spa » ===== [[w:Éléments de la période 3|Période 3]] ===== ''Pour : '''S'''odium, '''M'''a'''g'''nésium, '''Al'''uminium, '''Si'''licium, '''P'''hosphore, '''S'''oufre, '''C'''h'''l'''ore, '''Ar'''gon.'' * « '''S'''uzanne '''M'''an'''g'''ea '''Al'''lègrement '''Si'''x '''P'''russiens '''S'''ans '''Cl'''aquer '''A'''p'''r'''ès. » * « '''S'''uzanne '''M'''an'''g'''ea '''Al'''lègrement '''Si'''x '''P'''oulets '''S'''ans '''Cl'''aquer des '''Ar'''ticulations. » * « '''S'''uzanne '''M'''an'''g'''ea '''Al'''lègrement '''Si'''x '''P'''oulets (ou '''P'''erdrix) '''S'''ans '''Cl'''aquer d' '''Ar'''gent. » Le sodium est représenté par '''Na'''. Alors Napoléon remplace Suzanne pour retrouver le symbole : * « '''Na'''poléon '''M'''an'''g'''ea '''Al'''lègrement '''Si'''x '''P'''oulets '''S'''ans '''Cl'''aquer d''''Ar'''gent » — ou « sans claquer '''A'''p'''r'''ès », « d''''Ar'''gon », « d''''Ar'''tère », « (d')'''Ar'''tiche » ou « sans claquer les '''Ar'''ticulations » pour éviter la confusion avec l'élément argent, noté '''Ag'''. * « '''Na'''poléon '''M'''an'''g'''ea '''Al'''lègrement '''Si'''x '''P'''russiens '''S'''ans '''Cl'''ore l’'''Ar'''mistice. » * « '''Na'''guère '''M'''onsei'''g'''neur '''Al'''louche '''Si''' '''P'''ervers '''S'''uça '''Cl'''aire '''Ar'''demment. » * « '''Na'''poléon '''M'''an'''g'''eait '''Al'''lègrement '''Si'''x '''P'''oulets '''S'''ans '''Cl'''amser '''A'''p'''r'''ès. » * « '''Na'''poléon '''M'''a'''g'''nera '''À''' '''l'''<nowiki/>'est '''Si''' '''P'''ossible '''S'''a '''C'''o'''l'''onne '''A'''rmée. » *« '''Na'''billa '''M'''an'''g'''e (h)'''Al'''lal '''Si''' '''P'''atrick '''S'''ébastien '''Cl'''ash '''Ar'''thur. » ===== [[w:Éléments de la période 4|Période 4]] ===== ''Pour : '''K'''allium (Potassium), '''Ca'''lcium, '''Sc'''andium, '''Ti'''tane, '''V'''anadium, '''C'''h'''r'''ome, '''M'''a'''n'''ganèse, '''Fe'''r, '''Co'''balt, '''Ni'''ckel, '''Cu'''ivre, '''Z'''i'''n'''c, '''Ga'''llium, '''Ge'''rmanium, '''A'''r'''s'''enic, '''Sé'''lénium, '''Br'''ome, '''Kr'''ypton.'' * « '''K'''arl '''Ca'''pitaine '''Sc'''andinave '''Ti'''ra sa '''V'''erge '''Cr'''asseuse et '''M'''i'''n'''uscule, '''Fé'''conda le '''Co'''n de '''Ni'''cole, et le '''Cu'''l de ses '''Z'''e'''n'''nemies, '''Ga'''rdant '''Ge'''néreusement '''As'''sez de '''Se'''mence pour ce '''Br'''ave '''K'''h'''r'''ouchtchev. » * « '''K'''évin '''Ca'''pture un '''Sc'''arabée '''Ti'''mide dans le '''V'''agin '''Cr'''éatif de '''M'''o'''n'''ique, '''Fé''' (fait) '''Co'''mme '''Ni'''cole dans le '''Cu'''l en '''Zinc''' de '''Ga'''spard de '''Ge'''rmanie, puis '''As'''pire '''Sé'''bastien dans la '''Br'''aguette du '''Kr'''aken » * « '''K'''hrouchtchev '''Ca'''ressa '''Sc'''iemment '''Ti'''to. '''V'''orochev '''Cr'''ia '''M'''ag'''n'''anime : "'''Fé''' pas le '''Co'''n '''Ni'''kita, ton '''Cu'''l en '''Z'''i'''n'''c '''Ga'''lvanisé te '''Gè'''ne '''AsSe'''z pour '''Br'''anler des '''Kr'''evettes. » * « '''K'''épler '''Ca'''lculait des '''Sc'''alaires '''Ti'''tanesques, '''V'''oyant '''Cr'''o-'''M'''ag'''n'''on '''Fé'''sant le '''Co'''n '''Ni'''ché sur le '''Cu'''l d'un '''Z'''ébulo'''n''', '''Ga'''gnant '''Gé'''néralement '''AsSe'''z de '''B'''iè'''r'''es '''Kr'''onenbourg. » * « '''K''' '''Ca''' (cacas) '''Sc'''iés de '''Ti'''ti '''V'''olant et '''Cr'''os '''M'''i'''n'''et qui '''F'''ont ('''Fe''') des '''Co'''nneries ont '''Ni'''qué le '''Cu'''l à '''Z'''a'''n'''zibar d'un '''Ga'''rs '''Gé'''nial '''As'''sis '''Se'''rrant une '''B'''onne ('''Br''') '''Kr'''o. » * « '''K'''hrouchtchev '''Ca'''ressa '''Sc'''andaleusement '''Ti'''tov. '''Van'''ia '''Cr'''ia '''M'''ag'''n'''animement "'''F'''ais pas le '''Co'''n Nikita, la Cuisine en '''Z'''i'''n'''c de la '''Ga'''re de '''Ge'''nève '''As''' Ses '''Br'''iques '''Cr'''euses » (ou '''K'''hrouchtchev '''Ca'''ssa le '''SC'''ooter à '''TI'''tov) * « '''K'''hrouchtchev '''Ca'''ressa '''Sc'''andaleusement la '''Ti'''gnasse de '''V'''anadia, '''Cr'''oyant '''M'''a'''n'''ifestement '''Fe'''re '''Co'''cu '''Ni'''colaiev, le '''Cu'''ré de '''Z'''a'''n'''zibar '''Ga'''gna '''Ge'''nève '''As'''sez '''Se'''crètement avec Son '''Br'''éviaire '''Kr'''ipté. » * « '''K'''arl '''Ca'''valier '''Sc'''andinave '''Ti'''ra '''V'''engeance '''C'''r'''u'''elle '''M'''a'''n'''iant le '''Fe'''r '''Co'''ntre le '''Ni'''kel. Le '''Cu'''l de '''Z'''e'''n'''obie '''Ga'''rnit de '''Ge'''ranium '''As'''pire la '''Se'''ve '''Br'''ûlante du '''Kr'''atère (cratère/Krypton). » * « '''K'''onrad '''Ca'''pitaine '''Sc'''andinave '''Ti'''ra sa '''V'''erge '''Cr'''asseuse et '''M'''i'''n'''uscule, '''Fe'''rmant le '''Co'''n de '''Ni'''cole, le '''Cu'''l de '''Zn'''obie, et '''Ga'''rdant '''Ge'''néreusement l''''As'''permique '''Se'''mence du '''Br'''ave '''K'''e'''r'''mit. » * « '''K'''a'''Ca''' '''Sc'''quatte avec '''Ti'''ti la '''V'''oiture de '''Cr'''os '''M'''i'''n'''et. '''Fe'''rnand '''Co'''nduit sa mi'''Ni''' '''COOPER''' en '''Z'''i'''g'''za'''Ga'''nt, '''Ge'''néralement '''As'''sis en se '''Se'''rvant une '''B'''ière '''Kr'''onembourg. » * « '''K'''hrouchtchev '''Ca'''ressa '''Sc'''rupuleusement le '''Ti'''tanesque et '''V'''elue '''Cr'''ane du '''M'''o'''n'''de avec '''Fe'''rmeté. '''Co'''ntre l'ennemie, '''Ni'''kita '''C'''a'''u'''sa la '''Z'''iza'''n'''ie, il en'''Ga-Ge'''a l''''As'''sault '''S'''talinien. '''Br'''avo '''K'''h'''r'''ouchtchev. » * « '''K'''af'''Ca''' (Kafka) '''Sc'''ruta, '''Ti'''mide, '''V'''era '''Cr'''uz '''M'''o'''n'''trant ses '''Fe'''sses, '''Co'''mme la '''Ni'''mphe (nymphe) '''Cu'''pide '''Z'''é'''n'''a. '''Ga'''llien et '''Ge'''rard, '''As'''ssis, '''Se''' '''Br'''assaient de la '''Kr'''onenbourg. » * « '''K'''orrigan '''Ca'''pitaine '''Sc'''andinave '''Ti'''rant sa '''V'''erge '''Cr'''asseuse et '''M'''i'''n'''uscule ('''Mn''') '''Fe'''rma le '''Co'''n de '''Ni'''cole et le '''Cu'''l de '''Z'''é'''n'''obie ('''Zn''') '''Ga'''rdant '''Gé'''néreusement l’'''As'''permatique '''Se'''mence d’un '''Br'''un '''Kr'''omatique (chromatique).» *« '''K'''évin '''Ca'''sse '''Sc'''iemment sa '''Ti'''relire et '''V'''ient '''Cr'''ier : "'''M'''ama'''n''', fait ('''Fe''') '''Co'''mme '''Ni'''cole, '''Cu'''isine !". '''Z'''hi'''n'''g lui dit : "dé'''GaGe''', '''AsSe'''z '''Br'''aillé '''Kr'''étin". * « '''K'''arine '''Ca'''lcula que '''Sc'''ientifiquement '''Ti'''tillé, '''V'''incent '''Cr'''ame '''M'''o'''n''' '''F'''outr'''e''' '''Co'''mme '''Ni'''colas '''Cu'''pide '''Z'''i'''n'''zin '''Ga'''lleux '''Ge'''sticulant '''As'''ymétriquement et '''Se''' '''Br'''ulant à la '''Kr'''yptonite. » * « '''K'''ptain '''Ca'''ca '''Sc'''andinave '''Ti'''re sa '''V'''erge '''Cr'''asseuse et '''M'''i'''n'''uscule (Mn) des '''Fe'''sses de '''Co'''rine '''Ni'''çoise, '''Cu'''ltivée et '''Z'''e'''n''' (Zn), '''Ga'''lamment '''Ge'''néreuse, '''As'''sez '''Se'''xy et '''Br'''anlant '''K'''a'''r'''im (Kr). » * « '''K'''évin '''Ca'''tapulta '''Sc'''iemment '''Ti'''bère le '''V'''erreux, le '''Cr'''étin, le '''M'''écha'''n'''t, '''Fe'''roce, '''Co'''rrompu. Ha'''Ni'''bal, '''Cu'''i'''Z'''a'''n'''t, '''Ga'''ve '''Ge'''ntillement d''''As'''pirine '''Se'''c '''Br'''utus le '''Kr'''asseux. » * « '''K'''arine '''Ca'''ressa '''S'''e'''c''' '''Ti'''mothée '''V'''ers sa '''Cr'''oupe '''M'''ais '''n'''e '''Fé'''(fait) '''Co'''uille '''Ni''' '''Cu'''l. '''Z'''ho'''n'''g '''Ga'''gna '''Ge'''ntiment '''A s'''e '''Br'''anler '''Kr'''asseusement. » * « '''K'''oalas de '''Ca'''nberra, '''S'''’é'''c'''ria-'''T'''-'''i'''l, je '''V'''eux '''Cr'''oire '''M'''o'''n''' '''F'''r'''è'''re '''Co'''mplètement ! Ils '''Ni'''chent, '''C'''op'''u'''lent en '''Z'''o'''n'''ages '''Ga'''lamment '''Gé'''rés, '''As'''sistés '''Se'''ulement de '''Br'''ouillons '''K(r)'''yptés. (variante : de '''Br'''aves '''K'''angou'''r'''ous)» * '''K'''arl '''Ca'''usa '''Sc'''andale. '''Ti'''ens, '''V'''oilà '''C'''a'''r'''rément '''M'''o'''n'''sieur '''FeCoNi''' '''Cu'''ltivant '''Z'''o'''n''' '''Ga'''rdin. '''Ge'''rmaine, '''As'''sise, '''Se''' '''Br'''osse le '''Kr'''âne. * '''K'''hrouchtchev, '''Ca'''lant son '''Sc'''rotum '''Ti'''tanesque '''V'''ers la '''Cr'''oupe '''M'''ag'''n'''ifique des '''Fe'''sses de '''Co'''ssiga, '''Ni'''qua ses '''Cu'''isses dé'''Z'''i'''n'''guées: '''Ga'''lipettes '''Gê'''nantes, '''As'''sez '''Se'''xy pour '''Br'''anler '''Kr'''ouchtchev. * '''K'''rouchtchev, '''Ca'''libre '''Sc'''rotal '''Ti'''tanesque, '''V'''a '''Cr'''euser '''M'''o'''n''' '''Fe'''ssier '''Co'''quin. '''Ni'''quant '''Cu'''pidement '''Z'''i'''n'''oviev, '''Ga'''garine '''Gè'''re '''As'''sez '''Se'''s '''Br'''asseries '''Kr'''onenbourg. * '''K'''rouchtchev '''Ca'''resse '''Sc'''rotalement '''Ti'''to. «'''V'''oilà», '''Cr'''ie '''M'''arc (Mn) '''Fe'''uillée, «'''Co'''mment '''Ni'''kita '''Cu'''lbute!» «'''Z'''i'''n'''zin!» '''Ga'''léjea '''Gé'''rard, «'''As'''sez '''Se'''xy! '''Br'''avo, '''Kr'''ouchtchev!» * '''K'''rouchtchev '''Ca'''usa '''Sc'''andale. '''Ti'''to, '''V'''erge '''Cr'''amoisie, '''M''''e'''nFe'''rre '''Co'''chonnement les '''Ni'''bards et les '''Cu'''isses. '''Z'''é'''n'''obie '''Ga'''ve '''Gé'''rard, '''As'''pergeant '''Se'''s '''Br'''asseries '''Kr'''onenbourg. ===== [[w:Éléments de la période 5|Période 5]] ===== ''Pour : '''R'''u'''b'''idium, '''S'''t'''r'''ontium, '''Y'''ttrium, '''Z'''i'''r'''conium, '''N'''io'''b'''ium, '''Mo'''lybdène, '''T'''e'''c'''hnétium, '''Ru'''thénium, '''Rh'''odium, '''P'''alla'''d'''ium, '''A'''r'''g'''ent, '''C'''a'''d'''mium, '''In'''dium, '''S'''ta'''n'''num (Étain), '''S'''ti'''b'''ium (Antimoine), '''Te'''llure, '''I'''ode, '''Xé'''non.'' * « '''R'''o'''b'''in '''S'''u'''r''' '''Y'''vette a le '''Z'''èb'''r'''e '''N'''o'''b'''le de '''Mo'''nsieur '''T'''u'''c''' '''Ru'''. '''R'''o'''h'''an '''P'''ru'''d'''emment '''Ag'''é '''C'''é'''d'''a '''In'''évitablement '''S'''a'''n'''s '''S'''u'''b'''ir '''Te'''s '''I'''dées '''Xé'''nophobes. » (variante : '''R'''o'''b'''in '''S'''o'''r'''t '''Y'''von, le '''Z'''èb'''r'''e '''N'''o'''b'''le, dans '''Mo'''n '''T'''a'''c'''ot '''R'''o'''u'''illé) * «'''R'''o'''b'''ert '''S'''enio'''r''' est un '''Y'''éti du '''Z'''aï'''r'''e ou un '''N'''o'''b'''lio de '''Mo'''dène. Le '''T'''e'''c'''hnicien '''Ru'''dement bourré au '''Rh'''um '''P'''é'''d'''ale '''Ag'''ilement des '''C'''ou'''d'''es, il '''I'''nsulte '''S'''ai'''n'''t '''S'''e'''b'''astien, un '''Te'''rrien '''I'''diot et '''Xé'''nophobe. » * « '''R'''u'''b'''y, '''S'''o'''r'''te de '''Y'''éti '''Z'''aï'''r'''ois qu’un '''N'''o'''b'''liau de '''Mo'''dène, '''T'''e'''c'''hniquement '''Ru'''iné, '''R'''ac'''h'''ète au '''P'''ala'''d'''in avec l’'''A'''r'''g'''ent des '''C'''a'''d'''eaux '''In'''digènes '''S'''a'''n'''s '''S'''u'''b'''ir '''Te'''s '''I'''res '''Xé'''nophobes. » * « Le '''R'''a'''b'''bin '''S'''o'''r'''t son '''Y'''acht, le '''Z'''èb'''r'''e, pendant que '''N'''a'''b'''il, le '''Mo'''ldave '''T'''ur'''c''' '''Ru'''dement bourré au '''Rh'''um '''P'''é'''d'''ale '''Ag'''ilement des '''C'''ou'''d'''es, '''In'''diquant à '''S'''o'''n''' ami '''S'''é'''b'''astien la '''Te'''rre '''I'''mbibée de '''Xé'''rès. » * « '''R'''o'''b'''in '''S'''′'''r'''approche du '''Y'''éti sur un '''Z'''èb'''r'''e, '''No'''nobstant le '''Mo'''rse '''Tc'''hèque en '''Ru'''t et le '''Rh'''inocéros '''P'''é'''d'''é '''Ag'''ressif et '''C'''an'''d'''ide ; '''In'''capable de '''Sn'''iffer du '''S'''a'''b'''le et de la '''Te'''rre en '''I'''mitant '''Xé'''na. » * «'''R'''o'''b'''ert '''S'''enio'''r''', un '''Y'''éti du '''Z'''aï'''r'''e, '''N'''o'''b'''le et '''Mo'''rose, '''T'''ri'''c'''otait '''Ru'''e du '''Rh'''um et, '''P'''en'''d'''ant l′'''Ag'''enouillement du '''C'''i'''d''', '''In'''sulta '''Sn'''obes et '''Sb'''ires '''Te'''rrorisés à l′'''I'''rruption d′un '''Xé'''nophobe. » ===== [[w:Éléments de la période 6|Période 6]] ===== ''Pour : '''C'''é'''s'''ium, '''Ba'''ryum, '''La'''nthane, '''H'''a'''f'''nium, '''Ta'''ntale, '''W'''olfram (Tungstène), '''R'''h'''é'''nium, '''Os'''mium, '''Ir'''idium, '''P'''la'''t'''ine, '''Au'''rum (Or), '''H'''ydrar'''g'''irum (Mercure), '''T'''ha'''l'''lium, '''P'''lom'''b''', '''Bi'''smuth, '''Po'''lonium, '''A'''s'''t'''ate, '''R'''ado'''n'''.'' (La ou Lu selon la classification) * « '''C'''é'''s'''ar '''Ba'''lade '''La''' '''H'''i'''f'''i de '''Ta'''ta dans le '''W'''agon et '''Re'''garde '''Os'''si '''Ir'''ma. '''P'''e'''t'''er '''Au''' '''H'''an'''g'''ar, un '''T'''e'''l''' '''P'''ro'''b'''lème '''Bi'''en '''Po'''sé '''At'''tend '''R'''épo'''n'''se. » (variante : et '''Re'''garde '''Os'''ciller '''Ir'''ma) * « '''C'''é'''s'''ar '''Ba'''isa '''La'''ngoureusement l''''H'''orri'''f'''iante '''Ta'''ntouse dans les '''W'''C ('''Ré'''pugnants), '''Ré'''pétant les '''Os'''cillations '''Ir'''resistibles du '''P'''é'''t'''ard d''''Au'''rélien ; Mercure('''Hg''') lui '''T'''ai'''l'''la dans le '''P'''lom'''b''' une '''Bi'''te '''Po'''ilue pour lui '''A'''s'''t'''iquer les '''R'''ei'''n'''s. » * « '''C'''a'''s'''imir et '''Ba'''stien '''La'''ncent des '''H'''yper'''f'''réquences qui '''Ta'''pent sur un '''W'''agon '''Re'''mpli d''''Os''' '''Ir'''radiés, qui '''P'''é'''t'''a à l''''Au'''be '''H'''y'''g'''iénique, '''T'''é'''l'''éportant un '''P'''lom'''b'''ier '''Bi'''zarre '''Po'''lonais '''At'''taché à la '''R'''ei'''n'''e. » ===== [[w:Éléments de la période 7|Période 7]] ===== Pour : '''Fr'''ancium, '''Ra'''dium, '''Ac'''tinium, '''R'''uther'''f'''ordium, '''D'''u'''b'''nium, '''S'''eabor'''g'''ium, '''B'''o'''h'''rium, '''H'''a'''s'''sium, '''M'''ei'''t'''nérium, '''D'''arm'''s'''tadtium, '''R'''oent'''g'''enium, '''C'''oper'''n'''icium * « Les '''Fr'''ancais '''Ra'''lent '''Ac'''tivement depuis que '''R'''a'''f'''farin a '''D'''ou'''b'''lé '''S'''é'''g'''olène, é'''B'''a'''h'''ie par son '''H'''i'''s'''toire '''M'''ue'''t'''te sur la '''D'''i'''s'''tribution '''R'''é'''g'''ionale de la '''C'''o'''n'''nerie. » * « '''Fr'''anck '''Ra'''te '''Ac'''tuellement le '''R'''on'''f'''lement '''D'''é'''b'''ile du '''S'''i'''g'''nal '''B'''o'''h'''émien à l''''H'''i'''s'''toire '''M'''y'''t'''hique... » ===== [[w:Lanthanides|Lanthanides]] ===== ''Pour : ('''La'''nthane), '''Cé'''rium, '''Pr'''aséodyme, '''N'''éo'''d'''yme, '''P'''ro'''m'''ethium, '''S'''a'''m'''arium, '''Eu'''ropium, '''G'''a'''d'''olinium, '''T'''er'''b'''ium, '''Dy'''sprosium, '''Ho'''lmium, '''Er'''bium, '''T'''hulliu'''m''', '''Y'''tter'''b'''ium, '''Lu'''técium'' * « '''Cé'''dric, '''Pr'''ophète '''N'''éan'''d'''ertalien, '''P'''ro'''m'''et la '''S'''a'''m'''ba. '''Eu'''gène, '''G'''ran'''d''' '''T'''rou'''b'''le '''Dy'''namique des '''Ho'''mmes '''Er'''rants, '''T'''o'''m'''be sur l'h'''Yb'''ride '''Lu'''ne. » * « La '''Ce'''llule du '''Pr'''ofesseur est à '''N'''otre-'''d'''ame de '''P'''ana'''m'''e, '''S'''o'''m'''met de l' '''Eu'''rope, '''G'''ran'''d'''e et '''T'''rès''' b'''elle, où '''Dy'''onisos et '''Ho'''mère '''Er'''raient sans '''T'''u'''m'''ulte. '''Y'''a'''b'''on '''Lu'''tèce! » * « '''Ce'''cile, qui '''Pr'''atiquait le '''N'''u'''d'''isme, '''P'''ro'''m'''ettait à la '''S'''a'''m'''aritaine '''Eu'''phorique un '''G'''o'''d'''emichet avec un '''T'''u'''b'''e de '''Dy'''namite, '''Ho'''chet '''Er'''otique et '''T'''u'''m'''éfiant, s'''Y'''m'''b'''ole de '''Lu'''xure. » * « '''Ce'''sar se '''Pr'''omène avec '''N'''a'''d'''ine et '''P'''a'''m'''ina, en '''S'''e'''m'''ant les '''Eu'''nuques qui '''G'''ar'''d'''aient le '''T'''éné'''b'''reux '''Dy'''lan dans un cac'''Ho'''t car il '''Er'''rait près de la '''T'''o'''m'''be d''''Yb'''-'''Lu'''" * « '''Ce''' '''P'''a'''r'''adis que '''N'''ous '''d'''onna ('''Nd''') '''P'''ro'''m'''éthée, '''S'''e'''m'''blable à l''''Eu'''rope, nous '''G'''ar'''d'''e des '''T'''erri'''b'''les '''Dy'''sputes à l''''Ho'''rizon. Nous '''Er'''igerons une '''T'''o'''m'''be et '''Y''' '''b'''annirons '''Lu'''cifer » ===== [[w:Actinides|Actinides]] ===== ''Pour : ('''Ac'''tinium), '''Th'''orium, '''Pr'''otactinium, '''U'''ranium, '''N'''e'''p'''tunium, '''P'''l'''u'''tonium, '''Am'''ericium, '''C'''uriu'''m''', '''B'''er'''k'''élium, '''C'''ali'''f'''ornium, '''E'''in'''s'''teinium, '''F'''er'''m'''ium, '''M'''en'''d'''élénium, '''No'''bélium, '''L'''aw'''r'''encium.'' * « '''Th'''éo '''Pa'''rle '''U'''niversellement mais ''' N' '''ex'''p'''rime '''P'''l'''u'''s l''''Am'''ertume '''C'''o'''m'''mune. '''B'''roo'''k''' '''C'''on'''f'''ie l''''Es'''poir de '''F'''or'''m'''er un '''M'''on'''d'''e '''No'''uveau et '''L'''ib'''r'''e. » * « L''''Ac'''tivation''' Th'''ermique des '''Pa'''tates à l''''U'''ranium du '''N'''é'''p'''al en '''Pu'''rée '''Am'''ène, '''C'''o'''m'''me à '''B'''ang'''k'''ok, le '''C'''on'''f'''ort '''Es'''thétique d'une '''F'''a'''m'''ine un '''M'''i'''d'''i de '''No'''ël au '''L'''ibé'''r'''ia. (ou en bord de '''Lw'''oire selon les classifications) » * « '''Th'''or '''Pa'''rtit '''U'''ne '''N'''uit '''p'''our '''P'''l'''u'''ton, '''Am'''oureux de '''C'''a'''m'''ille. '''B'''er'''k'''eley, '''C'''ali'''f'''ornia '''Es'''perait '''F'''u'''m'''er '''M'''a '''d'''ouce et '''No'''ble '''L'''au'''r'''a.» ===== [[w:Alcalin|Alcalin]]s Groupe 1 ===== ''Pour : ('''H'''ydrogène, non alcalin), '''Li'''thium, '''Na'''trium (Sodium), '''K'''allium (Potassium), '''R'''u'''b'''idium, '''C'''é'''s'''ium, '''Fr'''ancium.'' * « ('''H'''eureux) dans le '''Li'''t de '''Na'''tacha, [[w:Khrouchtchev|'''K'''hrouchtchev]] '''R'''a'''b'''aissait '''C'''on'''s'''tamment son '''Fr'''oc. » * « '''L'''’'''i'''nter'''Na'''tionale '''K'''ommuniste '''R'''e'''b'''ute les '''C'''apitali'''s'''tes '''Fr'''ançais. » * « '''Li'''li '''N’a''' '''K''' '''R'''e'''b'''outonner '''C'''e'''s''' '''Fr'''ocs ('''Fr'''usques). » ===== [[w:Alcalino-terreux|Alcalino-terreux]] Groupe 2 ===== ''Pour : '''Bé'''ryllium, '''M'''a'''g'''nésium, '''Ca'''lcium, '''S'''t'''r'''ontium, '''Ba'''ryum, '''Ra'''dium.'' * « '''Bé'''bel(mondo) '''M'''an'''g'''eait du '''Ca'''ssoulet '''S'''u'''r''' un '''Ba'''teau '''Ra'''pide. » * « '''Bé'''bert '''M'''an'''g'''ea du '''Ca'''nard '''S'''u'''r''' un '''Ba'''teau-'''Ra'''dar. » * « '''Bé'''ta '''M'''an'''g'''ea du '''Ca'''ca '''S'''u'''r''' le '''Ba'''r de '''Ra'''bat (Maroc). » * « '''Bé'''atrice '''M'''an'''g'''ea une '''Ca'''rotte en '''S'''i'''r'''otant un '''Ba'''nana-split '''Ra'''vissant. » ===== Groupe 13 ===== ''Pour '''B'''ore, '''Al'''uminium, '''Ga'''llium, '''In'''dium, '''T'''ha'''l'''lium.'' *"'''B'''oris '''Al'''lait '''Ga'''mbader '''In''' '''T'''ou'''l'''ouse" ===== [[w:Cristallogène|Cristallogène]]s Groupe 14 ===== ''Pour : '''C'''arbone, '''S'''ilicium, '''Ge'''rmanium, '''S'''ta'''n'''num (Étain), '''P'''lom'''b'''.'' * « '''C'''es '''S'''imples '''Ge'''stes '''S'''eraie'''n'''t '''P'''ro'''b'''lématiques'''. »''' * « '''C''' 'est '''Si''' '''Gê'''nant '''S'''a'''n'''s '''P'''u'''b'''is. » ===== [[w:Pnictogène|Pnictogène]]s Groupe 15 ===== ''Pour : '''N'''itrogène (Azote), '''P'''hosphore, '''A'''r'''s'''enic, '''S'''ti'''b'''ium (Antimoine), '''Bi'''smuth.'' * « '''N'''e '''P'''as '''As'''tiquer '''S'''e'''b''' et sa '''Bi'''te. » * « '''N'''e '''P'''as '''As'''tiquer '''S'''o'''b'''rement le '''Bi'''zuth. » * « '''N'''e '''P'''as '''As'''tiquer le'''S b'''outs de '''Bi'''te. » * « '''N'''e '''PAs''' '''S'''a'''b'''rer Byzance('''Bi'''). » * " '''N'''e '''P'''as '''As'''soir '''S'''a'''b'''rina '''Bi'''zarrement" ===== [[w:Chalcogène|Chalcogène]]s Groupe 16 ===== ''Pour : '''O'''xygène, '''S'''oufre, '''Sé'''lénium, '''Te'''llure, '''Po'''lonium.'' * « '''O'''live '''S'''uce le '''Se'''xe '''Te'''ndu de '''Po'''peye. » * « '''O'''hh '''S'''uce moi le '''Se'''xe et les '''Te'''sticules '''Po'''ilus. » * « '''O'''scar '''S'''uce '''Se'''s '''Te'''sticules '''Po'''ilus. » * « '''O'''rgasme '''S'''ur le '''Se'''duisant '''Te'''odore '''Po'''ilu. » * « '''OS''' '''Se'''dimentaire '''Te'''rriblement '''Po'''li. » * « '''O'''h '''S'''acré '''Se'''igneur aux '''Te'''sticules '''Po'''lyèdriques. » * « '''O'''h '''S'''eigneur '''Sé''' (c'est) '''Te'''llement '''Po'''urri. » ===== [[w:Halogène|Halogène]]s Groupe 17 ===== ''Pour : '''F'''luor, '''C'''h'''l'''ore, '''Br'''ome, '''I'''ode, '''A'''s'''t'''ate.'' * « '''F'''ootball '''Cl'''ub de '''Br'''èles '''I'''ncapables d''''At'''taquer. » * « '''F'''ranck et '''Cl'''aude '''Br'''outent '''I'''rène '''A''' '''t'''able. » * « Une '''F'''issure '''Cl'''aviculaire '''Br'''isa tout '''I'''ntérêt d''''At'''taquer. » * « '''F'''outez '''Cl'''aire, qui '''Br'''anle '''I'''saac, car elle '''At'''tend. » * « '''F'''antastique, '''Cl'''aire '''Br'''anche '''I'''nstinctivement l''''At'''tache. » * « '''F'''erdinand '''Cl'''aque '''Br'''utalement '''I'''rène '''A''' '''t'''erre. » * « Les '''F'''ameuses '''Cl'''ochettes des '''Br'''ebis d''''I'''talie '''At'''tirent. » * « Le '''F'''ranc '''Cl'''ovis '''Br'''oie d''''I'''nnombrables '''At'''omes. » * « '''F'''élicie '''Cl'''aqua '''Br'''ian, '''I'''nnocent '''At'''tardé. » * « '''F'''olle '''Cl'''ara '''Br'''ave l''''I'''nvincible '''At'''hena. » ===== [[w:Gaz noble|Gaz noble]]s Groupe 18 ===== ''Pour : '''Hé'''lium, '''Né'''on, '''Ar'''gon, '''Kr'''ypton, '''Xé'''non, '''R'''ado'''n'''.'' * « '''He'''rcule '''Né'''gligea d’'''Ar'''racher le '''K'''o'''r'''sage de '''Xé'''na et '''R'''o'''n'''fla. » * « '''Hé''','''Né'''ron, '''Ar'''rête de '''Kr'''âner, '''Xé'''nophobe '''R'''i'''n'''gard ! » ===== [[w:Métalloïde|Métalloïde]]s ===== ''Pour : '''B'''ore, '''Si'''licium, '''Ge'''rmanium, Arsenic '''As''', Antimoine '''Sb''', '''Te'''llure et '''Po'''lonium'' *« '''B'''ob '''Si'''ffle son '''Ge'''t '''As'''sis avec '''S'''é'''b''' devant la '''Té'''lé '''Po'''lonaise. » ==== Couples acide/base ==== L’a'''c'''i'''d'''e '''c'''è'''d'''e un ou plusieurs protons tandis que la b'''a'''se c'''a'''pte un ou plusieurs protons. ==== Couples oxydant/réducteur ==== « Les électrons sont du côté de l'Occident. » (phonétiquement ''l’oxydant'') On peut également retenir que : **Ox Fixe, Red Cède (L'oxydant fixe des électrons, le réducteur en cède) ** l’oxyd'''ant''' est méch'''ant''' (il prend donc des électrons) ; ** le réduct'''eur''' a bon c'''œur''' (il donne donc des électrons). **L'oxydANT gagnANT, réductEUR donnEUR **Cap sur l'occident ! (L'oxydant '''cap'''te les électrons) **Notons aussi que l'oXydant aXepte (accepte) les électrons. Phrase qui marche à la fois pour les couples Acide/Base et Oxydant/Réducteur : L'Apéro gagne toujours ! ( A[cide] perd (des protons), O[xydant] gagne (des électrons) ). '''<nowiki/>''' ==== Ordre de priorité des groupements radicaux dans la nomenclature==== '''<nowiki/>''' '''A'''bruti '''H'''ans '''est''' l' '''ami de''' '''Nitr'''o! '''Al'''lez '''c'''hantons, L''''alcool''' '''am'''ené '''i'''ci '''e'''st '''t'''rès '''t'''errible Acide carboxylique, Halogénure, Ester, Amide, Nitrile, Aldéhyde, Cétone, Alcool, Amine, Imine, Ether, Thiol, Thioléter (pour ces deux derniers se reporter à la longueur). '''A Carbalo''' '''Ester''' '''a mit''' du '''Nitrile Aldéhyde''', '''s'étonne''' '''Alcolaminimine'''. '''Et tertio''', du '''thioleter'''. "L'''oïc''' '''est''' '''l’ami de Dalton":''' acide carboxylique (-oïque), ester, amide, aldéhyde, cétone. (ne pas confondre l'aldéhyde et l'alcool- voir la longueur des mots: c'est le plus long qui gagne). '''<u>Règle de Cahn, Ingold et Prelog</u>''' <u>''pour '''I '''> '''Br''' > '''Cl''' > '''S''' > '''F '''> '''O''' > '''N''' > '''C''' > '''H'''''</u> ** « '''Ib'''ra '''Cl'''ame '''S'''a '''F'''oi '''O''' '''N'''ouveau '''C'''avani '''H'''éroïque. » === Thermodynamique === ==== Loi des gaz parfaits ==== '''pV''' = '''nRT''' '''p''' = pression en pascals ; '''V''' = volume en mètres cubes ; '''n''' = quantité de matière en mols ; '''R''' = constante des gaz parfaits. R = 8,3 J.K-1.mol-1 ; '''T''' = température en Kelvins '''P'''ascal '''v'''oulut '''n'''ous '''r'''endre '''t'''héiste (référence au pari de Pascal) '''P'''a'''v'''a'''n'''e'''r'''ai'''t''' (sans les voyelles) '''pV''' = '''nRT''' n’est pas pété, énervé (ptnrv) '''P'''rocès-'''v'''erbal ; '''n'''ous '''r'''end '''t'''riste '''P'''uissance de '''V'''itesse = '''n'''otion de '''R'''apidité '''T'''errestre - Pour les joueurs de jeu de rôle uniquement ! Les PV d'un Pokemon est égale au Niveau fois sa RésisTance <br> ==== Différentielle de l’enthalpie ==== dH = TdS + VdP '''d'''îners '''H'''onorables = '''T'''artes '''d'''e '''S'''aison + '''V'''ins '''d'''u '''P'''ays (dH=TdS + VdP) '''d'''ouces '''H'''armonies = '''T'''oniques '''d'''e '''S'''olfège + '''V'''ibrations '''d'''e '''P'''iano (dH=TdS + VdP) '''d'''écouvertes '''H'''éroïques = '''T'''résors '''d'''e '''S'''able + '''V'''oyages '''d'''e '''P'''irates (dH=TdS + VdP) '''d'''anses '''H'''ispaniques = '''T'''angos '''d'''e '''S'''eville + '''V'''alses '''d'''e '''P'''ampelune (dH=TdS + VdP) '''d'''épart '''H'''éroïque = '''T'''oujours '''d'''u '''S'''tyle + '''V'''itesse '''d'''e '''P'''ointe (dH=TdS + VdP) '''d'''estination des '''H'''istoriens = '''T'''raversant '''d'''es '''S'''iècles + '''V'''oyageant '''d'''ans le '''P'''assé (dH=TdS + VdP) ==== Différentielle de l’enthalpie libre ==== dG = VdP-SdT '''V'''iande '''d'''e '''P'''orc '''-''' '''S'''el '''d'''e '''T'''able (VdP-SdT) '''V'''ends '''d'''u '''P'''ain sans (-) '''S'''ortir '''d'''e '''T'''on '''G'''îte (VdP - SdT = dG) ==== Différentielle de l’énergie interne (sans variation de quantité de matière) ==== dU=TdS-PdV '''T'''u '''d'''ois '''S'''avoir mais '''P'''as '''d'''e'''V'''iner (TdS-PdV) '''T'''out '''d'''e '''S'''uite '''Moins''' de '''P'''oints '''d'''e '''V'''ie '''T'''éter '''D'''u '''S'''el '''P'''endant '''D'''eux (ou '''D'''ix) '''V'''endredis. '''T'''rou '''d'''ans le '''S'''lip et '''P'''antalon '''d'''ans le '''V'''ent '''<nowiki>d'</nowiki>'''après '''U'''lysse = '''T'''outes '''d'''es '''S'''irènes mais '''P'''as '''d'''es '''V'''ampires <br /> ==== Différentielle de l’énergie interne (avec variation de quantité de matière) ==== dU=TdS-PdV+µdn '''T'''rop '''d'''e '''S'''avoir mais '''P'''as '''d'''<nowiki/>'en'''V'''ie c'est être '''nu''' '''d'''ans la '''n'''uit === Géologie === '''Les ères géologiques, du Quaternaire au Primaire (permettant de retenir les datations approximatives 7x60 + 3x40 Ma)''' '''Cénozoïque''' 60 (Quaternaire + Tertiaire) '''Crétacé''' 120 '''Jurassique''' 180 '''Trias''' 240 '''Permien''' 300 '''Carbonifère''' 360 '''Dévonien''' 420 '''Silurien''' 460 '''Ordovicien''' 500 '''Cambrien''' 540 '''C'''ite '''C'''e '''J'''oli '''T'''ruc '''P'''our '''C'''onnaître '''D'''es '''S'''iècles '''O'''rdonnés '''C'''orrectement ==== Niveaux de l'échelle chronologique géologique ==== '''Éo'''le '''ér'''adiqua les '''pe'''upliers '''ép'''uisés par l''''âge'''. * (éon, ère, période, époque, âge) ==== Les six périodes géologiques de l’ère primaire ==== ;Cambrien, Ordovicien, Silurien, Dévonien, Carbonifère, Permien. * '''''Cambr'''onne, l’'''ord'''urier,''' s’il eû'''t été '''dévo'''t, n’eût point '''carboni'''sé son '''pèr'''e'' * '''''Cambr'''onne '''ordo'''nna '''sil'''ence et '''dévo'''uement à ses '''car'''abiniers '''perm'''issionnaires'' * '''''Cambr'''onne '''aur'''ait, '''s'il eût''' été '''dévo'''t, '''carboni'''sé son '''pèr'''e'' * '''c-or-si-dé-ca-pé''' = Corps si décapés. * ''Le '''ca-or-sil-dé-ca-pe''' ='' Le Cahors, il décape. ==== Les trois périodes géologiques de l’ère secondaire ==== ;Trias, Jurassique, Crétacé. * '''T'''rois '''j'''ours '''c'''hacune. ==== Les cinq périodes géologiques de l’ère tertiaire ==== ;Paléocène, Éocène, Oligocène, Miocène, Pliocène. * '''Pâl'''e '''Et o'''bscène '''Au lit''', '''Mio''' se '''Plie au'''x scènes (de l'amour) ==== Stalactites et stalagmites ==== ''Les stalac'''t'''ites '''t'''ombent, les stalag'''m'''ites '''m'''ontent.'' ==== Géophysique ==== Formule pour la [http://fr.wikipedia.org/wiki/Anomalie_de_Bouguer correction gravitationnelle de Bouguer]: 2*π*h*ρ*G (G=constante gravitationnelle ρ=Masse volumique/Densité) "deux pies hachent Roger" 2 π h ρ G (ρ = "Rho", lettre grecque) ==== [[w:Échelle de Mohs|Échelle de Mohs]] ==== "'''T'''a '''G'''rosse '''C'''oncierge '''F'''olle d''''A'''mour '''O'''se '''Q'''uémander '''T'''es '''C'''aresses '''D'''ivines" "'''T'''oi '''G'''rand '''C'''hevalier, '''F'''uis '''A'''vec '''O'''rdre '''Q'''uand '''T'''on '''C'''œur '''D'''éfaille" "'''T'''rès '''G'''rand '''C'''hemin de '''F'''er '''A'''pache. '''O'''h ! '''Q'''uel '''T'''emps '''C'''e '''D'''imanche !" "'''T'''on '''G'''igolo '''C'''onte '''F'''leurette '''A''' '''(H)O'''rtense, '''Q'''ui '''T'''e '''C'''ocufie '''D'''iablement !" "'''T'''on '''G'''rand '''C'''ul '''F'''endu '''A''' une '''O'''uverture '''Q'''ue '''T'''u '''C'''aches '''D'''écemment" "'''T'''on '''G'''ros '''C'''ochon '''F'''ait '''A'''ïe '''O'''uille '''Q'''uand '''T'''u '''C'''ognes '''D'''essus" ([[w:Talc|'''T'''alc]], [[w:Gypse|'''G'''ypse]], [[w:Calcite|'''C'''alcite]], [[w:Fluorite|'''F'''luorite]], [[w:Apatite|'''A'''patite]], [[w:Orthose|'''O'''rthose]], [[w:Quartz (minéral)|'''Q'''uartz]], [[w:Topaze|'''T'''opaze]], [[w:Corindon|'''C'''orindon]], [[w:Diamant|'''D'''iamant]]) === Botanique === ==== Distinguer les hêtres des charmes ==== Le '''charme''' d''''Adam''' est d''''être''' à '''poil'''. ou encore: "Être à poils charme Adam" La feuille du charme a des dents (charme d’Adam) et la feuille du hêtre a des poils (être à poil). ==== Distinguer les principales espèces de pin ==== Les aiguilles du pin '''blanc''' sont groupées par '''5'''. '''Blanc''' a '''5''' lettres. Le pin '''rouge''' a des aiguilles groupées par '''2'''. Le mot '''rouge''' a '''2''' syllabes (s'il est suivit d'un mot commençant par une consonne en versification). Les aiguilles du pin '''noir''' sont en groupes de '''2'''. Dans le mot '''noir''', il y a '''2''' voyelles. ==== Distinguer les sapins des épicéas ==== Les sapins ('''''A'''bies'') ont des cônes '''a'''scendants, les épicéas ('''''P'''icea'') ont des cônes '''p'''endants. ==== Distinguer les cèdres ==== Le cèdre de l’'''A'''tlas a les pointes des branches '''a'''scendantes, le cèdre de l’Himalaya (''Cedrus '''d'''eodara'') '''d'''escendantes, le cèdre du '''L'''iban horizonta'''l'''es. ==== Distinguer les platanes des érables ==== pl'''A'''t'''A'''ne : feuilles '''A'''lternes '''É'''rable : feuilles oppos'''É'''es ==== Distinguer un Catalpa d’un Paulownia ==== P'''a'''ulowni'''a''' : deux feuilles par nœud (2 fois le a dans le nom) C'''a'''t'''a'''lp'''a''' : trois feuilles par nœud (3 a dans son nom) ==== Distinguer les feuilles de trèfle ==== Les feuilles du trèfle blanc ont de petites dents autour, celles du trèfle rouge ont des poils autour. Dents blanches, poils rouges (et non dents rouges, poils blancs !) ==== Distinguer les feuilles de trèfles de celles des luzernes ==== Les luzernes (''Medicago'') ont des pointes (= aiguilles, les médecins font des injections) au bout des folioles. ==== Distinguer les vesces des gesses ==== gesses ('''''L'''athyrus'') : l'alignement des points d'insertion des filets des étamines forme un angle droit avec le tube des étamines → L vesces ('''''V'''icia'') : il est oblique par rapport au tube ; on retrouve ce côté oblique dans la lettre V ==== Distinguer les knauties des scabieuses ==== '''k'''nautie : 4 ('''k'''atr’) pétales dans chaque fleur de l’inflorescence '''s'''cabieuse : 5 ('''s'''inq) pétales par fleur de l’inflorescence et des '''s'''oies sur le réceptacle ==== Distinguer les plantules de céréales dans un champ ==== BOAS : le '''b'''lé étant plus riche a des oreillettes, des poils et une ligule l’'''o'''rge a des oreillettes et une ligule l’'''a'''voine a une ligule le '''s'''eigle étant plus pauvre, n’a plus rien === Zoologie === ==== Ordre des cétacés ==== « '''C'est assez''', dit la '''baleine''', al'''ors que''' j'ai le '''dos fin''' je me '''cache à l'eau''' **baleine, orque, dauphin, cachalot, mais la liste est très incomplète. ==== Ordre des tatous ==== Les tatous font partie de l’ordre des ''Édentés'' car : "T’as tout sauf les dents !" === Biologie === ==== L'ordre hiérarchique de la classification de [[w:Taxinomie|taxinomie]] ==== Des Rats Essayent de Courir là Où Finissent les Grands Espaces (Raccourcis).<br /> DoRs EnCOre, la Famille GÈRe.<br /> Reste En Classe Ou Fais Grandes Études.<br /> Reste En Contact, Odile, Fais Gaffe, Émile ! (inspiré de la Cité de la peur, où Odile est attachée de presse et Émile tueur)<br /> Domaine, Règne, Embranchement, Classe, Ordre, Famille, Genre, Espèce, (Race). RECOFGE: Règne, Embranchement, Classe, Ordre, Famille, Genre, Espèce ==== Les [[w:bases azotées|bases azotées]] de l’ADN ==== '''À''' '''T'''on '''G'''rand '''C'''œur. '''A'''h '''T'''a '''G'''ueule '''C'''rétin '''À''' '''T'''able '''G'''rand '''C'''hef ('''ATGC''' : [[w:adénine|adénine]], [[w:thymine|thymine]], [[w:guanine|guanine]], [[w:cytosine|cytosine]]) ==== Intron/Exon ==== '''Int'''ron = '''Int'''rus ou '''int'''rusif, c'est la partie de nucléotide d'un gène qui est excisé de l'ARN lors de l'épissage, à l'inverse des '''exons.''' '''<nowiki/>''' ==== La séquence nucléotidique des télomères humains ==== '''T'''ous '''t'''es '''a'''mis se '''g'''avent de '''g'''énial '''g'''uarana. (TTAGGG) ==== Les différentes phases de la [[mitose]] ==== **le Prophète Athée (Pro Met A T) ** Je te '''ProMets''' de l''''Ana'''l au '''Telo''' ** '''ProMets''' à '''Anna''' de '''Tél'''éphoner ** '''P'''etit '''M'''ammifère '''À''' '''T'''éton ** '''P'''etit '''M'''atin '''A'''uprès de '''T'''oi ** '''P'''etit '''M'''artien '''A'''ttaque la '''T'''erre ** '''P'''etite '''M'''émé '''A''' '''T'''éléphoné ** '''P'''apa '''M'''aman '''A'''mour '''T'''oujours ** '''P'''our '''M'''on '''A'''mi '''Th'''omas ** '''P'''our '''M'''on '''A'''mour '''T'''oujours ** '''P'''our '''M'''on '''A'''nus '''T'''roué ** '''P'''etite '''M'''ademoiselle '''A'''ge '''T'''endre ** Promettante : '''Pro'''/'''met'''t/'''an'''/'''te''' ** '''PROMETANATELO''' ** ou ProMéthée est AnaTello (Se rappeler de la phrase Prométhée est un intello) ** '''P'''apa '''M'''ange '''A''' '''T'''able ** '''P'''ro des '''M''' '''A''' '''T'''hs ** '''P'''rof de '''M''' '''A''' '''T'''hs ** '''P'''ierre '''M'''angea des '''A'''nanas '''T'''ransgéniques ** '''P'''ouvoir '''M'''asculin '''A'''vant '''T'''out ** '''P'''aris-'''M'''arseille '''A''' '''T'''rotinette ** '''P'''our '''M'''émoriser : '''A'''voir '''T'''ravaillé ** '''P'''our '''M'''ieux '''A'''pprendre'''T'''out ** '''P'''ays les '''M'''oins '''avancés''' ** Le '''Pro'''f '''Met''' l' '''Âne''' devant la '''Tél'''é ('''P'''rophase, '''M'''étaphase, '''A'''naphase, '''T'''élophase) ** '''P'''auline '''M'''arche '''à''' la '''T'''équila ** TAMPI (à lire à l'envers) ** c'est PRoMEtteur An(un) inTello ** Papa Mange un Abricot Trop sucré (avec le s de sucré pour la synthèse qui suit la mitose G1 --> S --> G2) ==== Les différentes phases de la PROPHASE ==== '''Le''' '''Zi'''zi du '''Pachy'''derme a des '''Di'''mensions '''Dia'''boliques. (Leptotène, Zygotène, Pachytène, Diplotène, et Diacinèse) ou Letzplin (lepto/zygo) protége (pachy) didier (diplo/diacinèse) ou Le Zip à Didier ou Le zizi n'a pas de diarrhée ou Le zizi poilu du doyen ou Le zizi du pachyderme et du diplodocus sont différents ou Les Zizis Peuvent Devenir Durs ! ou '''Pré'''férer '''Le''' '''Zi'''zi du '''Pachy'''derme à celui du '''Diplo'''docus '''Dia'''bétique Pour préleptotène, leptotène, zygotène, pachytène, diplotène, diacinèse ==== Les acides aminés dits essentiels : ==== ''on compte neuf acides aminés essentiels : le tryptophane, la lysine, la méthionine, la phénylalanine, la thréonine, la valine, la leucine, l'isoleucine et l'histidine'' Le (LEU) trou (THR) de l'hystérique (HIS) Lyse (LYS) fait (PHE) tripper (TRY) valentin (VAL) mais (MET) ilose (ILE) pas ! '''''Le''' '''très''' '''ly'''rique '''Tri'''stan '''fait''' '''va'''chement '''m'''archer '''Ys'''eult, quelle '''Hist'''oire !'' ou encore : Hystérique, le très lyrique Tristan fait vachement méditer Iseult en Argentine (His)Leu-Thr-Lys-Trp-Phe-Val-Met-Iso(Arg): Histidine et Arginine seulement essentiels chez les enfants. ('''Le'''ucine, '''Thré'''onine, '''Ly'''sine, '''Try'''ptophane, '''Phé'''nylalanine, '''Va'''line, '''M'''éthionine, '''Is'''oleucine, '''Hist'''idine) Met le dans la valise, il fait trop d'histoire avec l'argent/en argentine. le cours d’'''hist'''oire, '''il''' '''le''' '''lit''' '''mais''' '''fait''' '''tres''' '''trivial''' ('''Hist'''idine; '''Ile''': Isoleucine, '''Leu'''cine, '''Ly'''sine, '''Mé'''thionine, '''Phé'''nylalanine '''Thré'''onine,'''Try'''ptophane,, '''Va'''line) Dans une '''V'''(aline)'''I'''(soleucine)'''L'''(eucine), il y a des '''H'''(ystidine)'''L'''(ysine)'''M'''(ethionine) et des '''P'''(hénilalanine)'''T'''(hréonine)'''T'''(ryptophane) (Dans une ville, il y a des HLM et des PTT) ''ils le valent trop trop mes félicitations'' '''ile''' '''leu''' '''val''' '''thr'''''op'' '''tr'''''o'''''p''' '''met''' '''phe''' '''lys''' ''itations'' ''Va te le mettre, Phillipe'' '''VA'''l '''TH'''r '''LE'''u '''MET''' '''TR'''p '''PH'''e '''ILE''' '''LY'''s pe '''''Va''' '''tri'''poter '''Lys'''e mais ('''met''') fait ('''phe''') '''le''' '''tr'''ès '''iso'''lément'' ''val thr lys met phe (fait) leu trp ile (iso-leucine)'' Plus simple et plus concret que tous les autres moyens mnémotechniques: VTT MILLPH (prononcé VTT MILF) et ainsi vous obtiendrez : Valine, Thréonine, Tryptophane, Méthionine, Isoleucine, Leucine, Lysine, Phénylalanine, Histidine. ==== Les acides aminés dits apolaire (Proline polaire/apolaire comprise) ==== Valérie promet à la triste Iseult le phénix et la Glycine. (val) (Pro/Met)(Ala)(Trp) (Ile) (Leu) (Phe) (Gly) Glycine dévale à la pelle, il le promet trop. ==== Le [[w:cycle de Krebs|cycle de Krebs]] ==== **''Si le '''citr'''on '''iso'''<nowiki>le l'</nowiki>'''acéto'''ne, le '''succi'''nct '''succès''' '''fumera''' '''m'''oins '''haut''''' ('''citr'''ate, '''iso'''citrate, alph'''acét'''oglutarate, '''succ'''inyl CoA, '''succ'''inate, '''fumara'''te, '''ma'''late, '''o'''xaloacétate) ** Avec les initiales : "C'est ici ce samedi soir : fumette, mal-à-la-tête, oubliette." ou encore : ** ''La '''C''' '''I''' '''A''' '''su'''specte un '''su'''spect qui '''fum'''e des '''Mal'''boros '''ox'''ydées.'' ** '''O'''h '''C'''atastrophe ! '''I'''l '''Os'''e '''Ac'''tiver '''Sa''' '''Su'''per '''F'''orce '''M'''agique ==== Le [[w:Cycle de Calvin|Cycle de Calvin]] ==== **"Les '''ri'''mes '''intermédiaires''' aux '''fo'''rmes '''diffo'''rmes de '''PGAL''' '''ri'''ment." ('''ri'''bulose phosphate, '''intermédiaire''' instable qui se scinde en deux 3-'''pho'''sphoglycérate, 1,3-'''dipho'''sphoglycérate, phosphoglycéraldéhyde ('''PGAL'''), dont l'un quitte le cycle et cinq sont utilisés pour reformer le '''ri'''bulose phosphate.) ==== Les Aldohexoses ==== '''Allo'''ns, '''altr'''uiste '''gl'''acer la '''mann'''e, '''Gul'''liver '''i'''ra '''gal'''érer au '''tall'''us ('''Allo'''se; '''altr'''ose; '''gl'''ucose; '''mann'''ose, '''Gu'''lose '''i'''dose '''ga'''lactose '''ta'''lose) ==== Les protéines intervenant dans les jonctions cellulaires ==== (Attention que ces phrases ne fonctionnent pleinement que si l'on connaît <i>déjà</i> les protéines intervenant, mais que l'on a du mal à retenir lesquelles font quoi.) - Jonction Adherens : '''Vin'''t le '''cad'''avre '''é'''quipé d''''a'''rmes '''α''' qui '''plaqu'''a le '''glo'''ussant '''ca'''valier. -> '''Vin'''culine, '''cad'''hérine-'''E''', '''a'''ctine, '''α'''-actinine, '''plak'''o'''glo'''bine, '''ca'''ténine. - Jonction de contact focal : '''Vin'''t la '''paix''' '''intégr'''ale; les '''a'''rmes '''α''' en '''ta'''s. -> '''Vin'''culine, '''pax'''iline, '''intégr'''ines, '''a'''ctine, '''α'''-actinine, '''ta'''line. - Desmosome : Tout ce qui '''colle''', plus la '''kératine'''. -> Desmo'''coll'''ine, desmo'''glé'''ine, desmo'''plak'''ine, '''plak'''oglobine, '''plak'''ophiline, '''kératine'''. - Jonction Gap (de communication) : Elle induit une '''connexion'''. -> '''Connex'''ines. - Hémidesmosomes : La '''p'''yramide de '''Khé'''ops '''d'''étruit '''intégr'''alement '''la mi'''en'''ne'''. -> '''P'''lectine, '''ké'''ratine 5 et 14, '''d'''ystonine, '''intégr'''ine α6β4, '''laminine''' 332. - Jonction tight, ou étanche, qui comporte des "'''kissing''' points" et dont le complexe crée une "'''zonula occludens'''" : '''Embrasser''' '''Claudine''' crée une '''occlu'''sion '''acti'''ve. -> Claudine, occludine, actine, ZO-1. ==== Les protéines des filaments intermédiaires ==== Elles diffèrent en fonction du tissu où elles se trouvent. Attention que les moyens proposés ici servent plus à retrouver la fonction d'une protéine déjà connue qu'à retenir le nom en lui-même. - Épithéliums : Kératines. Facile, il suffit de réfléchir un instant pour s'apercevoir que l'épithélium est bourré de kératines (couche cornée, desmosomes, ...). - Tissu '''C'''onjonctif : '''V'''imentines. On retient "'''CV'''". - Tissu '''M'''usculaire : '''D'''esmines. On retient "'''MD'''", une abréviation fréquente en anglais pour qualifier un Docteur en Médecine (Medicinæ doctor). - Tissu Nerveux proprement dit : Protéines des neurofilaments. Elles n'ont donc pas de nom propre, leur nom est leur fonction : des '''protéines''' dans les '''filaments''' intermédiaires des '''neuro'''nes. - Tissu Nerveux "de soutien" (tissu glial, donc) : Protéines fibrillaires acides gliales. Elles n'ont pas de nom propre, le nom est la fonction : Des '''protéines''' qui génèrent des '''filaments''' ('''fibrillaires''', donc) appartenant au tissu '''glial'''. La seule chose à retenir est qu'elles sont acides. - Noyaux : Lamines. On peut retenir qu'elles forment la '''lamina''' nucléaire, ou encore que pour arriver au noyau d'une cellule il faut la "maltraiter", et pourquoi pas la '''laminer'''. ==Technologie== ===Électronique=== ====Code couleur des résistances==== Code couleur à retenir : Noir, Marron, Rouge, Orange, Jaune, Vert, Bleu, Violet, Gris, Blanc * Ne Mangez Rien Ou Je Vous Battrai Violemment Gros Béta. * Ne Mangez Rien Ou Je Vous Brûle Votre Grosse Barbe. * Ne Mangez Rien Ou Jeunez Voilà Bien Votre Grande Bêtise. === Informatique === ==== RJ45 croisé ==== Broches 361782'''45'''. ==== Modèle OSI ==== Le modèle OSI divise les fonctionnalités nécessaires à la communication en sept couches : *# '''P'''hysique, *# '''L'''iaison, *# '''R'''éseau, *# '''T'''ransport, *# '''S'''ession, *# '''P'''résentation, *# '''A'''pplication. ** Il faut être deux pour avoir une liaison. ** Le 4*4 est un transport. "Félicie, OSI" *#Séduit par son '''PHYSIQUE''' *#et n'ayant aucune '''LIAISON''', *#je l'ai contactée sur un '''RÉSEAU''' social. *#Arrivé chez elle en '''TRANSPORT''' en commun, *#suivi une '''SESSION''' de va-et-vient, *#sans aucune forme de '''PRÉSENTATION''', *#j'y ai mis toute mon '''APPLICATION'''. Le lendemain, elle me recontactait... Les mots des phrases suivantes ont des initiales identiques à celles des couches, dans l'ordre ci-dessus ( P L R T S P A ) : *#'''P'''artout '''L'''e '''R'''oi '''T'''rouve '''S'''a '''P'''lace '''A'''ssise *# '''P'''our '''L'''e '''R'''éseau '''T'''out '''S'''e '''P'''asse '''A'''utomatiquement *# '''P'''ar '''L'''à, '''R'''aisonnons '''T'''ransport '''S'''ans '''P'''résenter l''''A'''pplication *# ''Pour les amateurs du jeu d'échecs :'' '''P'''rends '''L'''a '''R'''eine '''T'''out '''S'''era '''P'''lus '''A'''gréable *# '''P'''etit '''L'''apin '''R'''ose '''T'''rouvé à la '''S.P.A.''' *# '''P'''our '''L'''e '''R'''epas '''T'''out '''S'''era '''P'''rêt '''À''' 7 heures (7 couches) *# '''P'''our '''L'''e '''R'''éseau '''T'''u '''S'''eras '''P'''as '''A'''ugmenté *# '''P'''our '''L'''a '''R'''oute, '''T'''u '''S'''uis '''P'''ierre-'''A'''lain ! *# '''P'''our '''L'''a''' R'''etenir '''T'''oujours '''S'''e '''P'''arler '''A'''vant ! Les mots des phrases suivantes ont des initiales identiques à celles des couches, dans l'ordre inverse ( A P S T R L P ) : *# '''A'''près '''P'''lusieurs '''S'''emaines, '''T'''out '''R'''espire '''L'''a '''P'''aix ==== Classe d'adresse IP ==== En binaire, compter le nombre de 1 avant le premier 0. * A : 0 -> 127 (+ 127) <code>0xxxxxxx</code> * B : 128 -> 191 (+ 63) <code>10xxxxxx</code> * C : 192 -> 223 (+ 31) <code>110xxxxx</code> == Grammaire et orthographe == ===<u>Les principaux mots interrogatifs</u>=== Ce moyen mnémotechnique est très utile pour les coups de téléphone où l’on doit demander des renseignements. Il faut dresser rapidement la liste des mots interrogatifs sur un papier et être sûr que l’on a des réponses à toutes les questions. '''C’est cucu, c’est occupé !''' *'''C'''ombien ? *'''Q'''uoi ? *'''Q'''ui ? *'''C'''omment ? *'''O'''ù ? *'''Q'''uand ? *'''P'''ourquoi ? === [[w:Conjonction de coordination|Conjonctions de coordination]] === ''Mais où est donc Ornicar ?'' (Mais, Ou, Et, Donc, Or, Ni, Car) Mais cette méthode est pédagogiquement discutable, car elle entretient la confusion entre ''et'' (conjonction) et ''est'' (verbe ''être'' à la troisième personne du singulier), ainsi qu'entre ''ou'' (conjonction) et ''où'' (adverbe ou pronom relatif). Attention, ''donc'' n’est plus une conjonction de coordination, mais bien un verbe conjugué pour ''est'' et un adverbe de coordination pour ''où'' ! On peut aussi l’apprendre de cette manière afin de sortir le OU et ne pas induire de confusion dans l’esprit Mais ! Et donc Ornicar (mais, et, donc, or, ni, car) en jouant sur la sonorité de la surprise Au Québec, on dit aussi: ''Mais où est donc Carnior ?'' ===<u>Les principales prépositions</u>=== *''Adam part pour Anvers avec cent sous sûrs, entre derrière chez Decontre'' :(À, Dans, Par, Pour, En, Vers, Avec, Sans, Sous, Sur, Entre, Derrière, Chez, De, Contre) *''Adam part pour envers avec deux cents sous chez Parmisur. '' :(À, Dans, Par, Pour, En, Vers, Avec, De, Sans, Sous, Chez, Parmi, Sur) *''Adam part pour Anvers avec deux cents sous.'' *"Adam part pour Anvers avec deux cents sous chez Sur." :( À, Dans, Par, Pour, En, Vers, Avec, De, Sans, Sous, Chez, Sur) *''Adam part pour Anvers avec cent sous de chez surdurand.'' *"(À, Dans, Par, Pour, En, Vers, Avec, Sans, Sous, De, Chez, Sur, Durant) *''Adam Surché part pour Anvers avec deux-cents sous'' *"(À, Dans, Sur, Chez, Par, Pour, En, Vers, Avec, Sans, Sous) *’’ Adeudans part pour Sur sans sous chez Devant-derrière avec Avant-après-contre’’ ===<u>Les pronoms relatifs</u>=== 3 culs domptent ouvertement monsieur lequel,Duquel,Auquel... ''<nowiki>Qui que quoi dont où lequel duquel auquel…'</nowiki>'' ===<u>Les déterminants possessifs</u>=== Au pluriel: ''Mais c'était nos voleurs !'' (mes, ses, tes, nos, vos, leurs) ===<u>Orthographe</u>=== * Mou'''r'''ir ne prend qu’un “ r ” car on ne meurt qu’une fois. * Nou'''rr'''ir prend deux “ r ” car on se nourrit plusieurs fois. * Cou'''r'''ir ne prend qu’un “ r ”car on manque d’air en courant,<br /> mais quand on a'''rr'''ive on prend tout l’air qu’on peut. * L’hironde'''ll'''e prend deux “ l ” car elle vole avec ses deux ailes. * La v'''i'''e'''i'''lle ne peut marcher qu’avec ses deux bâtons. * A'''pp'''uyer prend deux « p » car on s’appuie mieux sur deux pattes. * Un ba'''l'''ai prend un seul “ l ” car il n’y a qu’un manche. * Un ba'''ll'''et prend deux “ l ” car pour danser il faut deux jambes. * Toujour'''s''', toujours un “ s ” et à jamai'''s''', ne jamais l’oublier. * J’a'''p'''erçois sur une jambe mais j’a'''pp'''arais sur les deux. * Quand je mets deux "p" à apercevoir, j'aperçois une faute. * Je n’a'''p'''erçois qu’un '''p''' à a'''p'''ercevoir (ou je m’a'''p'''erçois qu’a'''p'''ercevoir ne prend qu’un '''p'''). * Enve'''l'''oppe ne prend qu’un “ l ” car on ne met qu’une lettre dans une enveloppe. En revanche pour un vélo on a deux '''p'''neus : dé''velo'''''pp'''er, en''velo'''''pp'''er, etc. * Cuiss'''eau''' de v'''eau'''. * Sate'''ll'''ite prend 2 '''L''' car c’est plus pratique pour voler. (et un seul '''t''' car il ne tourne qu’autour d’une seule '''T'''erre) * Évide'''mm'''ent prend deux '''m''' comme dans '''Papa/Maman''' (à noter : tous les adjectifs qui se terminent par "ent", comme "évident", prennent 2 "m" ensuite, comme "évidemment"). * Je me souviens d’une corde en rappel : On se souvient '''DE''' quelque chose, mais on se rappelle quelque chose. * Un pa'''r'''esseux cou'''r'''onné ca'''r'''essait une ca'''r'''otte avec un air intéressé : liste de mots qui ne prennent qu’un '''r'''. * Co'''ll'''ine a deux colonnes (2 ” l ”) et colo'''nn'''e a deux collines (2 ” n ”). * Dé'''velopp'''er je fais du vélo avec mes deux pieds pour pédaler. * Échapper prend deux "P" car on s'échappe mieux avec deux pieds. * Culo'''tt'''e prend deux '''T''' car il y a deux jambes pour une culotte * Un professeur a un seul '''F'''ront et deux '''S'''ourcils, donc un seul F, mais deux S * Philippe : je marche (2p) mais ne vole pas (1l) * On parle le flaman'''D''' dans les Flan'''D'''res. Le flaman'''T''' rose est un oiseau de grande '''T'''aille. * L’am'''a'''nde pousse sur un '''a'''rbre ; l’am'''e'''nde sur un '''e'''ssuie-glace. *Guè'''r'''e signifie "pas beaucoup", donc un seul r. Il faut au moins deux adversaires pour faire la gue'''rr'''e, donc 2 R. * Une P'''ê'''che (melba...) / P'''ê'''cher (du poisson) / P'''é'''cher (commettre une offense) / un P'''é'''ché (originel...) : Dans la p'''ê'''che en rivière, le '''^''' représente l’hameçon et la p'''ê'''che (fruit) représente le flotteur de la canne à p'''ê'''che). Quand on confesse au prêtre un p'''é'''ché, on fait profil bas (accent aigu sur le '''é'''). * Tous les membres de la famille ont un accent grave, sauf pépé et mémé : père, mère, nièce… * M devant Mbappé (M devant M, B et P) * Reg versus Erg : ** Un Reg est un désert de Roches, de pieRRes ** L’ERg est un désERt de dunes ===<u>Mots avec accent circonflexe</u>=== * Une t'''a'''che c'est suffisamment sale pour ne pas avoir besoin d'en rajouter (d'accent circonflexe) * Le chapeau de c'''i'''me est tombé dans l’ab'''î'''me. Et celui du bo'''i'''teux dans la bo'''î'''te ! * On dit chapeau ! pour la '''tâche''' accomplie et non pas chapeau ! pour la '''tache''' sur le vêtement. * Un chien ou un chat marche sur deux paires de pa'''tt'''es. Par contre, on fait cuire des p'''ât'''es dans une casserole qu'on couvre avec le chapeau du â. * "Traîner ses guêtres", c'est flâner. <u>Pour les anglophones</u>, il suffit souvent de comparer le mot anglais de même racine que le mot français sur lequel on a un doute pour l'accent circonflexe. Si ce mot anglais contient un S, le mot français équivalent contient souvent un accent circonflexe. Exemples : * Ancêtre / Ancestor * Apôtre / Apostle * Arrêt / Arrest * Bâtard / Bastard * Bête / Beast * Boîte / Box * Château / Castle *Cloître / Cloister * Côte (anatomie, rivage, pente, culinaire) / Coast (rivage) * Coût / Cost * Crête / Crest (vague, cimier, huppe…) * Dégoût / Disgust * Épitre / Epistle * Fête, Festif (fr) / Feast (eng) * Guêpe / Wasp (eng) tous deux venant de vespa (latin) * Forêt / Forest * Hâtif / Hasty * Hôpital / Hospital * Hôte, hôtesse / Host, hostess * Hâte / Haste * Honnête / Honest * Huître / Oyster * Île / Island * Intérêt / Interest * Maître / Master * Mât / Mast (bateau) * Paître / To pasture * Pâtisserie, Pâte / Pastry / Pasta (ital.) * Plâtre / Plaster * Quête / Quest * Rôtir / To roast * Tâche (travail et non salissure) / Task * Tempête / Tempest ===<u>Pluriel</u>=== *Pluriel en OUX au lieu de OUS ::Un '''hibou''' moche comme un '''pou''' ::Avait pour '''joujou''' sur ses '''genoux''' ::Un '''caillou''' aussi '''chou''' qu’un '''bijou'''. Variante : ::Viens mon '''chou''', mon '''bijou''' ::Viens sur mes '''genoux''' ::Avec des '''joujoux''' et des '''cailloux''' ::Pour éloigner ces vilains '''hiboux''' pleins de '''poux''' Variante : ::Viens mon '''chou''', mon '''joujou''', mon '''bijou''' ::Sur mes '''genoux''' ::Jeter des '''cailloux''' ::À ces vieux '''hiboux''', pleins de '''poux''' Variante :<blockquote>Viens mon '''chou''', sur mes '''genoux''' avec tes '''joujoux''' et tes '''bijoux'''</blockquote><blockquote>Pour jeter des '''cailloux''' sur les vilains '''hiboux''' pleins de '''poux'''.</blockquote> Variante : Répéter plusieurs fois très vite : Hi-ge-jou-bi-ca-chou-pou. Vous avez ainsi les premières syllabes des 7 noms qui se terminent en "oux" au pluriel. 2e variante: '''J'''e '''P'''eux '''B'''oire '''C'''omme '''C'''es '''G'''ros '''H'''ommes. * Les noms terminés en « -al » font leur pluriel en « -aux » (sauf ''aval, bal, cal, carnaval, chacal, choral, festival, mistral, naval, pal, récital, régal''… qui font leur pluriel en « s ») : ::Dans mon pays '''natal''' ::Où les gens sont pourtant '''joviaux''' ::Eut lieu, c’était '''fatal''', un combat '''naval''', ::Heureusement, ce fut le combat '''final''' ::Parce qu’il faisait '''glacial'''. == [[w:Grammaire|Langues étrangères]] == Ces langues nous sont étrangères, d’où l’importance de trouver des moyens mnémotechniques === [[w:Allemand|Allemand]] === ==== Liste des particules verbales non détachables ==== ''J’ai mis Cerbère en enfer'' : ge-, miss-, zer-, be-, er-, ent-, emp-, ver- ''Cerbère gémit en enfer'' : zer-, be-, er-, ge-, miss-, ent-, emp-, ver- ''Miss Verzer bégaie en panthère'' : miss-, ver-, zer-, be-, ge-, emp-, ent-, er- ==== Genre des mots ==== Les mots (de plus d'une syllabe) se terminant en -e, -ei, -ie, -heit, -keit, -tion, -ung sont féminins. <br /> Il existe bien sûr des exceptions : der Däne, das Genie, der Ursprung, der Hochsprung... === [[w:Anglais|Anglais]] === ==== Mots contraires ou confondables ==== * Left: gauche / '''R'''ight: d'''r'''oite ** avec la''' main gauche''', on peut former un '''L''' en tenant les doigts en haut et le pouce en avant. C’est le '''L''' de '''L'''eft. ** dans l’alphabet le '''L''' est à '''gauche''' ('''L'''eft) et le '''R''' est à '''droite''' ('''R'''ight) : **:A B C D E F G H I J K '''L''' M N O P Q '''R''' S T U V W X Y Z ** Copy'''right''' veut dire '''droit''' d'auteur. ** Quand on est a'''droit''', c’est bien (= '''right''' en anglais) * Odd (3 lettres) : impair / Even (4 lettres) : pair ** Tuesday : mardi / Thu'''r'''sday : jeudi *** Thu'''r'''sday est le quat'''r'''ième jour de la semaine, il a donc un '''r''' ('''quatrième''' lettre) *** En classant les mots dans l’ordre lexicographique : ****jeudi (Thursday) est avant mardi (Tuesday), ****Thursday (jeudi) est avant Tuesday (mardi). *** Je'''u'''di et Th'''u'''rsday ont tous les deux la lettre '''u''' en troisième position. *** étymologiquement : Thursday = jour de '''Thor''', et jeudi = jour de '''Jupiter''' (Jovis die). Thor (mythologie nordique) et Jupiter (mythologie romaine) sont tous les deux ''dieu du tonnerre''. Même chose pour l'étymologie de Tuesday (jour de '''Tyr''') et de mardi (jour de '''Mars'''), tous les deux étant ''dieu de la guerre''. Mais il est plus difficile de retrouver Odin dans Tuesday. ***TUEsday sonne comme two-sday two étant égal au nombre 2 et mardi est le deuxième jour de la semaine. === [[w:Espagnol|Espagnol]] === ==== [[w:Consonne|Consonnes]] doublées ==== Les seules consonnes que l’on peut trouver à l’écrit en double sont celles du mot CaRoLiNa. On peut remarquer que le "[[w:LL|ll]]" est une consonne à part entière. Attention ! Ne pas confondre N et Ñ ==== <u>Subjonctif</u> ==== ===== verbe Savoir (saber) ===== si tu '''sé '''ton présent mais que tu ne '''sepa '''ton subjonctif ce n'est pas grave! === [[w:Japonais|Japonais]] === {{article détaillé|Japonais/Hiragana/Leçon 1}} === [[w:Latin|Latin]] === ==== Ordre des six cas principaux du latin ==== '''No'''us '''Vo'''us '''Ac'''hetons '''Gé'''néralement '''D'''es '''Ab'''ricots '''No'''minatif, '''Vo'''catif, '''Ac'''cusatif, '''Gé'''nitif, '''D'''atif, '''Ab'''latif === [[w:Néerlandais|Néerlandais]] === ==== Liste des particules verbales non détachables ==== ''begeherontverer' : be-, ge-, her-, ont-, ver-, er-'' BEnoit et Gerard ONT HERité du VERgER ==== Conjugaison de l’[[w:imparfait|imparfait]] ==== On forme l’[[w:imparfait|imparfait]] avec un '''t''' si le radical (Verbe -EN) du verbe se termine par F, K, P, S, T, CH.<br> Retenez : '''F'''ran'''K'''lin '''p'''rend '''s'''on '''t'''hé '''ch'''aud. <br> Si le radical se termine par une autre lettre, on forme l’imparfait avec un '''d'''. Exemples : <br> - pakken (''prendre'') : pak'''k'''-en > hij pak'''t'''e (''il prenait'')<br> - ruilen (''échanger'') : rui'''l'''-en > hij ruil'''d'''e (''il échangeait'') === [[w:Russe|Russe]]=== ==== Verbes à voyelle alternante ы/о dans le thème ==== '''К'''арл '''Р'''о'''М'''а'''Н'''о'''В''' крыть « couvrir » - рыть « creuser » - мыть « laver » - ныть « gémir; faire mal » - выть « hurler » Se fléchissent tous sur le modèle : * infinitif : мыть (accent stable au passé: м'ыла) * conjugaison : мóю, мóешь... мóют Contrairement à ст'ыть, ст'ыну « refroidir » ; слыть, слывý « être réputé... » == Littérature == === Auteurs français du {{XVIIe siècle}} === ''Sur une racine de la bruyère, une corneille boit l’eau de la fontaine Molière'' ([[w:Jean Racine|Racine]], [[w:Jean de La Bruyère|Jean de La Bruyère]], [[w:Pierre Corneille|Pierre Corneille]], [[w:Nicolas Boileau|Nicolas Boileau]], [[w:Jean de La Fontaine|Jean de La Fontaine]], [[w:Molière|Molière]]) Variante : La Corneille perchée sur la Racine de La Bruyère, Boileau de La Fontaine Molière ''(Remarque : La fontaine Molière est une fontaine à Paris)'' == Théâtre == Face à la scène, le côté '''j'''ardin et le côté '''c'''our sont du côté de chaque initiale de '''J'''ésus '''C'''hrist, de '''J'''ules '''C'''ésar , de '''J'''acques '''C'''artier ou de '''J'''acques '''C'''hirac (J.C. : jardin à gauche, cour à droite) Face au public, c’est l’inverse, et le côté '''cour''' est le côté du [[w:cœur|cœur]], à gauche. == Musique == '''Ah ! Lala !''' * Se souvenir de cette interjection pour faire correspondre les notes musicales latines (do, ré, mi fa...) avec les anglo-saxonnes (C, D, E, F...) '''A''' correspond à '''la''', il n'y a plus qu'à suivre B=si, C=do, D=ré, E=mi, F=fa, G=sol. '''TS MS DSS''' *''Nom des degrés'': '''t'''onique, '''s'''us-tonique, '''m'''édiante, '''s'''ous-dominante, '''d'''ominante, '''s'''us-dominante, '''s'''ensible '''Sa mère la racaille ! Saleté de fumier !''' *''Ordre des bémols'' : '''S'''i '''m'''i '''l'''a '''r'''é '''s'''ol '''d'''o '''f'''a '''Six mille laquais repus songent au dodo, fatigués''' *''Ordre des bémols'' : '''Si''' '''mi''' '''la''' '''ré''' '''so'''l '''do''' '''fa''' '''Facteur, donne au soldat réjoui la missive''' *''Ordre des dièses'' : '''Fa''' '''do''' '''sol''' '''ré''' '''la''' '''mi''' '''si''' '''Dommage, la mine est cassée Do Majeur --> La mineur''' *"Gamme relative" de Do Majeur '''Il Doit Posséder Les Modes En Lui''' Ionien, Dorien, Phrygien, Lydien, Mixolydien, Éolien, Locrien == Géographie == === Points cardinaux === ==== Où est l’est ? ==== *Visualiser Strasbourg et Brest. Strasbourg est à l'est, Brest est à l'ouest * Penser au mot '''O'''rang'''E''', sur une carte, l'Ouest est à gauche et l'Est à droite. * Écrire "'''où est''' l''''est'''" --> ouest à gauche et est à droite (en considérant le nord en haut bien entendu) * L’ouest est à gauche, l’est à droite (si le nord est au-dessus). * Penser que si on regarde la France, à gauche c'est l' ''eau'' comme dans '''O'''uest et à droite c'est l' ''étranger'' comme dans '''E'''st. * Penser qu'en France, on parle des "pays de l'est" (à droite sur la carte) et en parlant de la conquête de l'ouest on pense à l'Amérique (à gauche sur la carte) * Penser à : Ouest, le suffixe "est" se trouve à droite, tout comme l’est. Ce qui signifie "ouest" à gauche et "est" à droite. * Écrire ONE (1 en anglais) : Ouest-Nord-Est * Penser au mot 'OiE': l’Ouest est à gauche comme le O et l’Est est à droite comme le E (si le Nord est en haut) * Dans le mot ouest il y a un "u" comme dans gauche. Dans le mot est il n'y a pas de "u" comme dans droite. * Retenir le mot NESO en tournant dans le sens des aiguilles d'une montre, car l'inverse donne la nausée (NOSE). ==== Le soleil se couche à l’est ou à l’ouest ? ==== * Penser qu’en France, on voit de beaux couchers de soleil sur nos côtes atlantiques, à l’ouest. * Penser aussi au pays du soleil levant, le Japon, qui est bien à l’est du continent * Ou encore : Le Soleil se l'''è'''ve à l’'''e'''st et se c'''ou'''che à l’'''ou'''est === Pays limitrophes de la France === Aime '''I''S''A''B''E''L''A''' (Aimer pour la lettre '''M''') '''M'''onaco, '''I'''talie, '''S'''uisse, '''A'''llemagne, '''B'''elgique, '''E'''spagne, '''L'''uxembourg, '''A'''ndorre. ''MAL BAISÉ'' '''M'''onaco, '''A'''ndorre, '''L'''uxembourg, '''B'''elgique, '''A'''llemagne, '''I'''talie, '''S'''uisse, '''E'''spagne. ''AIMABLES'' '''A'''llemagne, '''I'''talie, '''M'''onaco, '''A'''ndorre, '''B'''elgique, '''L'''uxembourg, '''E'''spagne, '''S'''uisse Avec l’océan Atlantique, la Manche et la Méditerranée en plus : '' '''O'''h '''MA''' '''MER'''veilleuse '''BALISE''' '' '''O'''h = '''O'''céan Atlantique '''MA'''='''MA'''nche '''MER'''= '''MER'''Méditerranée '''BALISE'''= '''B'''elgique '''A'''llemagne '''L'''uxembourg '''I'''talie '''S'''uisse '''E'''spagne PS cette liste est valable seulement pour la France Métropolitaine car le pays avec lequel la France a la plus longue frontière est le ... Brésil ! (car la Guyane est un département français) === Grands lacs de l'Amérique du Nord === ''SMHEOL'' (d'ouest en est) '''S'''upérieur, '''M'''ichigan, '''H'''uron, '''E'''rié, '''O'''ntario (et St '''L'''aurent) ''HOLMES (élémentaire !)'' '''H'''uron, '''O'''ntario, St '''L'''aurent, '''M'''ichigan, '''E'''rié, '''S'''upérieur. Une autre méthode, souvent enseignée dans les cours de géographie, fait appel au mot anglais ''foyers'' de la manière suivante : ''HOMES'' '''H'''uron, '''O'''ntario, '''M'''ichigan, '''E'''rié, '''S'''upérieur === Les 5 arrondissements de New-York (Boroughs) === Vous n'arrivez pas à vous souvenir des 5 arrondissements de la grande ville de New-York? En sachant qu'il est incontournable d'aller se promener dans les grandes avenues et rues sans s'arrêter dans un stand BBQ et y manger les bons hot-dogs d'un sympathique New-yorkais. Il faut dire: '''Si Man BBQ''' (Staten Island, Manhattan, Brooklyn, Bronx, Queen). Bon appétit, bonne visite!. === Pays baltes === Vous confondez les pays baltes sur la carte ? C'est tout simple, ils sont placés par ordre alphabétique du nord au sud... [[w:Estonie|Estonie]], [[w:Lettonie|Lettonie]], [[w:Lituanie|Lituanie]] * Cela fonctionne aussi avec les appellations anglo-saxonnes : Estonia, Latvia, Lithuania et aussi avec les noms locaux : Eesti, Latvija, Lietuva. * Pour les capitales de ces pays : Estonie [[w:Tallinn|Tallinn]], Lettonie [[w:Riga|Riga]] et Lituanie [[w:Vilnius|Vilnius]] '''T'''rafic '''R'''outier '''V'''olumineux === Les tropiques === Les tropiques du Cancer et du Capricorne sont classés du nord au sud par ordre alphabétique. Le capricorne coule (en bas) car il a plus de lettres, il est plus lourd. L'antarctique aussi : l'arctique flotte. ou tropique du caNcer : N represente le Nord. ou Capricorne sonne comme "Cap Horn" donc au Sud === Les pays d'Amérique Centrale === *Du nord au sud : [[w:Bélize|Bélize]], [[w:Guatemala|Guatemala]], [[w:Honduras|Honduras]], [[w:Salvador|Salvador]], [[w:Nicaragua|Nicaragua]], [[w:Costa Rica|Costa Rica]], [[w:Panama|Panama]]. BGHSNCP soit: '''B'''eau '''G'''arçon '''H'''abitant '''S'''alvador '''N'''ettoie et '''C'''i'''r'''e les '''P'''lanchers ou Belle Guatemalaise Habitant Salvador, Nage sur la Côte du Panama. === Fleuves de Russie === D'ouest en est, les initiales des cinq principaux fleuves de [[w:Russie|Russie]] forment le mot "VOILA" : [[w:Volga|Volga]], [[w:Ob|Ob]], [[w:Ienisseï|Ienisseï]], [[w:Léna|Léna]], [[w:Amour (fleuve)|Amour]]. L'Ob, le Ienissei et la Léna sont les trois plus grands cours d'eau de Sibérie. === Pays du Moyen-Orient === Le Qatar est une presQu'île dans la péninsule arabiQue. Le Qatar peut être vu comme comme une Crête de CoQ juchée sur l'Arabie saoudite et s'ouvrant sur le golfe arabo-persiQue. === Pays d'Asie Centrale (en -stan) === * Du Nord au Sud et de l'Ouest à l'Est '''Kaz'''akhstan - '''Ou'''zbékistan - '''Ki'''rghizistan - '''Tu'''rkménistan - '''Ta'''djikistan - '''Af'''ghanistan - '''Pa'''kistan Kaz Ou Ki Tu Ta Af Pa Kazouki, tu taffes pas ? === Principales villes traversées par la Loire === * De l´Atlantique au Mont Gerbier de Jonc '''Na'''thalie '''an'''goisse '''to'''ujours les '''bl'''ondes '''or'''iginaires de '''Nevers''', elles '''ro'''ugissent '''sa'''ns '''pu'''deur. Nantes Angers Tours Blois Orléans Nevers Roanne Saint-Étienne Le Puy en Velay == Histoire == === Préhistoire === {{loupe|#Les périodes géologiques de l’ère primaire|# Les périodes géologiques de l’ère secondaire}} ==== Évolution des [[w:Homininae|homininés]] ==== Les '''Austral'''iens '''habil'''es eurent une '''érec'''tion, quand ils aperçurent, dans le '''néan'''t, des '''sapins''' gigantesques, <br /> ce qui donne, par ordre d'apparition<br /> [[w:Australopithèque|Australopithèque]], [[w:Homo habilis|Homo habilis]], [[w:Homo erectus|Homo erectus]], [[w:Homme de Néanderthal|Homme de Néanderthal]] et [[w:Homo Sapiens|Homo Sapiens]]. === Les 7 Merveilles du monde antique === "'''Mostapha''' ! '''J’attends''' la '''copie''' !" Variante: "'''Mostapha''' ! '''J’attends''' ta '''coloscopie''' !" ('''Mau'''solée d’Halicarnasse, '''Sta'''tue de Zeus à Olympie, '''Pha'''re d’Alexandrie, '''Ja'''rdins suspendus de Babylone, '''Tem'''ple d'Artémis à Éphèse, '''Co'''losse de Rhodes, '''Py'''ramides d’Égypte) === Les 7 rois de Rome === ''Ronutuann' tarsertar'' (qu'on retient mieux en imaginant le paresseux boucher Ronu : "Ronu, tuant tard, sert tard") ('''Ro'''mulus, '''Nu'''ma Pompilius, '''Tu'''llus Hostilius, '''An'''cus Martius, '''Tar'''quin l’Ancien, '''Ser'''vius Tullius, '''Tar'''quin le Superbe) === Les 11 [[w:Liste des empereurs romains|premiers empereurs romains]], dans l’ordre de leur règne === ''AuTiCaClauNéGalOViVesTiDo'' ('''Au'''guste, '''Ti'''bère, '''Ca'''ligula, '''Clau'''de, '''Né'''ron, '''Gal'''ba, '''O'''thon, '''Vi'''tellus, '''Ves'''pasien, '''Ti'''tus, '''Do'''mitien) Et les six suivants : ''NeTraHadAnMarCo'' (Nerva, Trajan, Hadrien, Antonin, Marc-Aurèle, Commode) Cesautica Clonegalo Vivestido CESar, AUguste, TIbere, CAligula CLAUde, NEron, GALba, Othon VItellus, VESpasien, TItus, Domitien === Les traités napoléoniens, dans l’ordre de leur signature === '''''CAV''''' (penser à une ''cave'') : ''Cambalu, Apresti, Viparis'' ('''Cam'''po Formio, '''Bâ'''le, '''Lu'''néville, '''A'''miens, '''Pres'''bourg, '''Ti'''lsit, '''Vi'''enne, '''Paris''') === Les présidents de la troisième République française === ''Thimagré Carcafauloufa Poindemidoudoule'' ('''Thi'''ers, '''Ma'''c-Mahon, '''Gré'''vy, '''Car'''not, '''Ca'''simir-Perier, '''Fau'''re, '''Lou'''bet, '''Fa'''llières, '''Poin'''caré, '''De'''schanel, '''Mi'''llerand, '''Dou'''mergue, '''Dou'''mer, '''Le'''brun) OU ce petit poème : Tire Mon Glaive Car Casse-Pierre Fort Loup Faillit Point Dèche Mille Dômes D'où Merle Brun === Les présidents de la cinquième République française === '''D'''es '''P'''illards '''G'''ouvernent, '''M'''ais '''C'''hacun '''S'''ubit '''H'''élas la Macronie '''D'''ur '''P'''armesan '''G'''orgonzola '''M'''ozzarella '''Ch'''auds '''S'''ervis. '''D'''ouce '''P'''atrie '''G'''auloise où '''M'''iaulent les '''Ch'''ats '''S'''iamois '''H'''eureux. <math>\Longrightarrow</math>De Gaulle, Pompidou, Giscard d'Estaing, Mitterrand, Chirac, Sarkozy, Hollande. '''D'''ouce '''P'''atrie '''G'''auloise où '''M'''iaulent les '''Ch'''ats '''S'''iamois '''H'''eureux et '''M'''alades. <math>\Longrightarrow</math>De Gaulle, Pompidou, Giscard d'Estaing, Mitterrand, Chirac, Sarkozy, Hollande, Macron. - Le sauveur de la France, Charles '''de Gaulle''', s’orthographie avec deux L. On peut retenir que de Gaulle a deux L, comme les deux barres de la croix de Lorraine, symbole de la France libre. On retient que la Gaule n’a qu’un L comme le L unique dans Celtes. Car "les Gaulois" est le nom que Jules César donne aux Celtes. === Les présidents américains à partir de Roosevelt === '''R'''udy '''T'''ente '''E'''n '''K'''araté '''J'''e '''N'''ique '''F'''abienne '''C'''omme '''R'''udy '''B'''ouche '''C'''ontre '''B'''ouche '''O'''utré '''T'''errifié. '''R'''oosevelt '''T'''rouva '''E'''léonore en '''K'''imono, '''J'''ames '''N'''e '''F'''ilma '''C'''arrément '''R'''ien, '''B'''rave '''C'''améraman '''B'''ouché et '''T'''êtu ! <math>\Longrightarrow</math>Roosevelt, Truman, Eisenhower, Kennedy, Johnson, Nixon, Ford, Carter, Reagan, Bush, Clinton, Bush, Obama, Trump. === Les dirigeants de l'URSS et de la Russie === '''L'''aissant '''S'''on '''K'''imono '''B'''leu '''À''' '''T'''rois '''G'''amins, '''E'''lle '''P'''ut '''M'''aintenir '''P'''outine. L = Lénine, S = Staline, K = Khrouchtchev B = Brejnev, À = Andropov, T = Tchernenko, G = Gorbatchev.....la "virgule" marque la chute du communisme, et E = Eltsine, P = Poutine, M = Medvedev, P = Poutine == Médecine == === Plan des muscles complexus === 1. Muscle semi-épineux de la tête * Sème tranquillement 156 graines dans un carré de terre pour 71 arbres épineux. (Le semi-épineux a pour origine les processus transverses de Th1 à TH5/Th6 et C4 à C7 et se termine sur les processus épineux de C7 ) th1) 2. Muscle longicissimus du cou *Louons tranquillement une sainte tu (la) sauteras. (Le muscle longicissimus du cou a pour origine les processus transverses de Th1 à Th5 et se termine sur les tubercules post de C3 à C7) === Les os du carpe === *''SSPP - TTCC'' (Initiales) * ''Sca-Lu-Py-Pi T-T-Go-Oc'' (Phonétique) <math>\Longrightarrow</math>('''Sca'''phoïde, (Semi-'''Lu'''naire)'''lu'''natum* , '''Py'''ramidal, '''Pi'''siforme - '''T'''rapèze, '''T'''rapézoïde, '''C'''apitatum ''', '''os '''C'''rochu ''')''' NB : dans la nouvelle nomenclature ce n'est plus le semi-lunaire mais le LUNATUM *On peut le voir sous un autre angle : ** PI - TRI - LU - SCA *: (pisciforme) (triquetrum) (lunatum) (scaphoide) ** HA - CA - TRI - TRA *: (hamatum) (capitatum) (trapézoide) (trapèze) Ou encore prendre les consonnes de ces 2 mots : *'''P'''é'''T'''a'''L'''e'''S''' : '''P'''isiforme - '''T'''riquetrum - '''L'''unatum - '''S'''caphoïde * a'''TT'''a'''CH'''e : '''T'''rapèze - '''T'''rapézoïde - '''C'''apitatum - '''H'''amatum *Trouvé par un étudiant : * ** '''S'''a'''L'''e '''T'''e'''P'''u, '''<nowiki>T'</nowiki>'''é'''T'''ais à '''CH'''ier. ** '''S'''uce '''l'''a '''t'''rique '''P'''atrick, '''t'''u '''t'''ireras '''Ch'''arlotte ** Le '''S'''carabée à '''L'''unettes '''T'''rie ses '''P'''ièces, '''T'''out '''T'''as est un '''C'''apital ('''h''')Amassé" ** '''S'''a'''L'''u'''T''' '''P'''ierre, '''T'''<nowiki/>'é'''T'''ais '''C'''haud '''H'''ier. === Les muscles épicondyliens médiaux (ex-épitrochléens) du membre supérieur === ''Grand Papa cuve et ronfle'' <math>\Longrightarrow</math>('''Grand pa'''lmaire, Petit '''pa'''lmaire, '''Cu'''bitus antérieur, '''Ron'''d pronateur, '''Flé'''chisseur commun superficiel) Une autre phrase est proposée "Grand Papa, Petit Papa, fléchit rondement le cul en avant" <u>Avec la nouvelle nomenclature</u> : ''Paulo Fuck Les Filles Sans Défense Faisant Un CAprice'' ''==> rond '''P'''ronateur, '''F'''léchisseur radial du carpe, '''L'''ong palmaire, '''F'''léchisseur '''S'''uperficiel des '''D'''oigts, '''Fl'''échisseur '''U'''lnaire du '''Ca'''rpe'' === Les muscles épicondyliens latéraux (ex-épicondyliens) du membre supérieur === <math>\Longrightarrow</math>'''2'''ème '''Ra'''dial, '''Ext'''enseur '''commun''', '''ext'''enseur '''propre''' '''du 5'''ème doigt, '''Court Su'''pinateur, '''Cu'''bital '''Post'''érieur, '''Anconé''' Deux rats excommuniés, expropriés du 5e ont cousu (court supinateur) le cul de la postière en cône. <u>Avec la nouvelle nomenclature</u> : ''Charlie Rêve d'Explorer Des Etoiles, 5 Etoiles Uniques Au Sanctuaire'' ''==>'' Court extenseur Radial du carpe, Extenseur commun des Doigts, Extenseur du 5ème doigt, Extenseur Ulnaire du carpe, Anconé, Supinateur === Les 12 paires de nerfs crâniens === ==== Ancienne nomenclature ==== *'''''O'''h '''O'''scar, '''m'''a '''p'''etite '''t'''héière '''m'''e '''f'''ait '''à''' '''g'''rand '''p'''eine '''s'''ix '''g'''rogs'' *'''''O'''h '''O'''scar, '''m'''a '''p'''etite '''t'''hérèse '''m'''e '''f'''ait '''à''' '''g'''rand '''p'''eine '''s'''ix '''g'''osses'' <math>\Longrightarrow</math>'''O'''lfactifs, '''O'''ptiques, '''M'''oteur oculaire commun, '''p'''athétiques, '''T'''rijumeau, '''M'''oteur oculaire externe, '''F'''aciaux, '''A'''uditifs, '''G'''losso-pharyngiens, '''P'''neumogastriques, '''S'''pinaux, '''G'''rand hypoglosse. * Nouvelle nomenclature *'''''OL'''ivia '''OPT'''<nowiki>e pour l'</nowiki>'''OC'''éan c'est '''TRO'''p '''TRI'''<nowiki>ste d'</nowiki>'''A'''ller '''FA'''ire des '''V'''isites '''G'''avantes quand les '''VAGUES''' '''A'''<nowiki>pportent l'</nowiki>'''HYP'''nose.'' *'''O'''h '''O'''h ! '''O'''scar ! '''T'''a '''T'''héière '''A''' '''F'''ait '''V'''ingt '''G'''rands '''V'''erres '''A''' '''H'''ector. *'' '''Ol'''ivier '''Op'''oil '''Ocul''' '''Troqu'''a '''Tri'''stement '''A'''vec '''Fa'''nny '''V'''ingt '''Gloss Par'''fums '''Va'''nille '''Accessoire'''<nowiki> d'</nowiki>'''Hy'''dratation.'' *'''''Ol'''é '''O'''<nowiki>scar d'</nowiki>'''Occ'''ident ! '''Tr'''availle ton '''Tri'''ceps, tes '''Abd'''o et tes '''F'''esses au '''WC'''. '''Gl'''isse '''vague'''ment ton '''accessoire''' et '''hip''' !'' *'''''O'''yez, '''O'''yez ! '''O'''bstinée '''T'''ortue '''T'''enace '''A''' '''F'''inalement '''V'''aincu, '''G'''rand '''V'''antard '''A''' '''H'''onte.'' *'''''O'''yé! '''O'''yé! '''O'''bstinée '''T'''ortue '''T'''enace '''A''' '''F'''inalement '''V'''aincu (la) '''G'''rande '''V'''ague '''À''' '''H'''awaii''. *'' '''Ol'''af '''Opt'''a '''Occ'''asionellement pour le '''Tro'''quet '''T'''andis qu' '''Abd'''el '''Fa'''isait '''V'''alser '''G'''rand'''-P'''ère '''Vague'''ment '''A'''utour de l' '''Hippo'''campe. '' *'''''O'''n '''O'''ccasion '''O'''livier '''T'''ries '''T'''o '''A'''nally '''F'''uck '''V'''arious '''G'''uys, '''V'''aginas '''A'''re '''H'''istory''. *'''''Ol'''a! '''Op'''hélie '''O cul Tro'''p '''Tri'''pant '''A Fa'''it '''Co'''quettement '''Glo'''usser '''Va'''lentin '''A''' l'<nowiki/>'''Hypo'''drome.'' <math>\Longrightarrow</math>'''O'''lfactif, '''O'''ptique, '''O'''cculomoteur, '''T'''rochléaire, '''T'''rijumeau, '''A'''bducens, '''F'''acial, '''V'''estibulo-Cochléaire ''ou'' '''C'''ochléo-vestibulaire, '''G'''losso-Pharyngien, '''V'''ague, '''A'''ccessoire, '''H'''ypoglosse. ==== Sensitif ou moteur ==== - Un dernier pour savoir la '''composante de chaque nerfs''' : Mots commençant par un '''S''' = sensitif, '''M''' = moteur, '''B''' = les deux (both). Ensuite les noms communs et les adjectifs sont parasympathiques (Money, brother, big, boobs). *'''''S'''ome '''S'''ay '''M'''oney '''M'''atters, '''B'''ut '''M'''y '''B'''rother '''S'''ays : '''B'''ig '''B'''oobs '''M'''atter '''M'''ost.'' ex: Boobs = 10e nerf (Vague), B = sensitif et moteur, nom commun = parasympathique. Ceci correspond aux caractéristiques du nerf vague ! - Une autre plus rigolote et moins décente : *'''''S'''eb '''S'''uces '''M'''oi '''M'''es '''D'''eux '''M'''amelons '''D'''e '''S'''ilicone '''D'''é-'''D'''é '''M'''e '''M'''anque'' '''S'''ahara '''S'''ablonneux (et) '''M'''er '''M'''orte, '''D'''eux '''M'''ondes '''D'''e '''S'''ilence (et) '''D'''éserts '''D'''e '''M'''ouvants '''M'''irages '''S''' = sensitif, '''M''' = moteur, '''D'''= les deux. Ainsi le 1{{er}} nerf crânien est sensitif. Le 9{{e}} et le 10{{e}} sont à la fois sensitifs et moteurs, etc. === Les 15 collatérales de l’artère maxillaire === '''''T'''on '''m'''épris '''p'''eut '''a'''mener '''m'''a '''t'''empête '''p'''etite '''b'''iche '''t'''ant aimée. '''Un''' '''p'''etit '''c'''âlin '''p'''eut '''p'''ardonner'' <math>\Longrightarrow</math>('''T'''ympanique, '''M'''éningée moyenne, '''P'''etite méningée, '''A'''lvéolaire inférieure, '''M'''asseterine, '''T'''emporale profonde postérieure, '''P'''térygoïdienne, '''B'''uccale, '''T'''emporale profonde antérieure, '''A'''lvéolaire supérieure, '''In'''fra-orbitaire, '''P'''alatine descendante, du '''C'''anal ptérygoïdien, '''P'''térygo-palatine, '''P'''haryngienne) (N.B. Non! l'artère pharyngienne est une branche de la carotide externe!) les 15 branches dans l'ordre: un '''T'''ic '''MENING'''é '''PE'''ut '''DE'''venir '''MA'''léfique, '''T'''andis qu'un '''BU'''bon '''TE'''rriblement '''AL'''gique '''P'''eu'''T''' être '''SOU'''lagé '''VI'''te '''PA'''r une '''PT'''yaline '''SP'''écifique '''( T'''ympanique, '''MENINGE'''é moyenne, '''PE'''tite méningée, '''DE'''ntaire inférieure, '''MA'''ssétérine, '''T'''emporale profonde moyenne, '''BU'''ccale, '''TE'''mporale profonde antérieure, '''AL'''véolaire, '''PT'''érygoïdienne, '''SOU'''s orbitaire, '''VI'''dienne, '''PA'''latine descendante, '''PT'''érygopalatine, '''SP'''hénopalatine.) === Les branches de l'artère axillaire === '''TH'''éodore '''a''' '''m'''angé '''s'''on '''c'''a'''c'''a <math>\Longrightarrow</math>'''TH'''oracique supérieure, '''A'''cromiothoracique, '''M'''ammaire Interne, '''S'''capulaire et les deux '''C'''irconflexes Cette artère se trouve dans la région de l'aisselle et pas ailleurs. === Branche de l'artère carotide externe : === '''''T'''ous '''L'''es '''F'''rançais '''O'''nt '''A'''pplaudi le '''P'''résident '''M'''onsieur '''T'''hiers''. <math>\Longrightarrow</math>('''T'''hyroïdienne supérieure, '''L'''inguale, '''F'''aciale, '''O'''ccipitale, '''A'''uriculaire postérieure, '''P'''haryngienne ascendante, '''M'''axillaire interne, '''T'''emporale superficielle): === Collatérales de la carotide externe === '''''T'''ire '''l'''a '''f'''icelle, '''p'''ortier ! '''O'''uvre '''à''' '''t'''on '''m'''aître '''r'''apidement'' <math>\Longrightarrow</math>('''T'''hyroïdienne supérieure, '''L'''inguale, '''F'''aciale, '''P'''haryngienne ascendante, '''O'''ccipitale, '''A'''uriculaire postérieure, '''T'''emporale superficielle, '''M'''axillaire, '''R'''ameau parotidien) '''T'''ou'''s''' '''l'''es '''F'''rançais '''a'''cclament '''O'''bama '''p'''résident <math>\Longrightarrow</math>('''T'''hyroïdienne '''s'''upérieure, '''L'''inguale, '''F'''aciale, '''A'''uriculaire postérieure, '''O'''ccipitale, '''P'''haryngienne ascendante) '''S'''ome '''a'''ngry '''l'''ady '''f'''igured '''o'''ut '''p'''ost '''m'''enopausal '''s'''yndrom. <math>\Longrightarrow</math>('''S'''uperior thyroidal, '''A'''scending pharyngeal, '''L'''ingual, '''F'''acial, '''O'''ccipital, '''P'''osterior auricular, '''M'''axillary, '''S'''uperficial temporal) Ce dernier, bien qu'en anglais, a l'avantage d'indiquer les artères dans l'ordre ascendant. '''T'''u '''P'''eux '''L'''a '''F'''ourrer '''O'''u l'''A''' '''M'''anger '''T'''oute. <math>\Longrightarrow</math>('''T'''hyroïdienne supérieure, '''P'''haryngée ascendante, '''L'''inguale, '''F'''aciale, '''O'''ccipitale, '''A'''uriculaire postérieure, '''M'''axillaire, '''T'''emporale superficielle) Ce dernier est une variante québécoise à caractère sexuel qui a également l'avantage d'indiquer les artères dans l'ordre ascendant. === Les collatérales de l’artère ophtalmique === '''''R'''emets '''l'''es '''c'''apotes '''s'''ans '''n'''ous '''f'''aire '''p'''<nowiki>erdre l'</nowiki>'''é'''rection'' <math>\Longrightarrow</math>(Centrale de la '''R'''étine, '''L'''acrymales, '''C'''iliaires, '''S'''upra-orbitaire, '''N'''asales, '''F'''rontales, '''P'''alpébrales, '''E'''htmoïdales antérieures et postérieures) === Les rameaux du plexus lombaire === '''Il''' '''hyp'''notise '''il'''lico l''''igu'''ane '''gé'''ant et '''fé'''roce et lui '''c'''oupe '''l'''ittéralement '''la''' '''cuisse''' qui '''fai'''sait '''ob'''struction '''car''' elle '''l'omb'''rageait. <math>\Longrightarrow</math>('''il'''io-'''hyp'''ogastrique; '''il'''io-'''ingu'''inal; '''gé'''nito-'''fé'''moral; '''c'''utané '''l'''atéral de '''la''' '''cuisse'''; '''fé'''moral; '''ob'''turateur; du muscle '''car'''ré des '''lombes''') === Les rameaux du plexus sacré === '''Si''' un '''gl'''acier '''sup'''er '''inf'''idèle est '''honteux''' d’avoir '''per'''foré le '''rect'''um et '''élev'''é '''l’anus''' d’un '''cu'''isinier '''po'''urtant '''c'''onsentant, un '''curé''' '''fe'''ra un '''ju'''gement '''im'''partial en '''oub'''liant l’'''in'''acceptable. <math>\Longrightarrow</math>('''sci'''atique; '''gl'''utéal '''sup'''érieur et '''inf'''érieur; '''honteux'''; '''pir'''iforme; '''rect'''al supérieur ; '''élév'''ateur de '''l’anus'''; '''cu'''tané '''po'''stérieur de la '''c'''uisse; '''carré''' '''fé'''moral et '''ju'''meau '''in'''férieur; '''ob'''turateur '''in'''terne) === Les muscles du grand trochanter === Mon '''g'''ars, '''troqu'''ons: Le '''Petit''' '''Pierre''' et '''les jumeaux''' '''moyen'''nant '''fesses''' à '''obtur'''er. <math>\Longrightarrow</math>('''G'''rand '''Troch'''anter: '''petit''' fessier; '''pir'''iforme; '''jumeaux''' supérieur et inférieur; '''moyen fess'''ier; '''obtur'''ateurs internes et externes) === Les muscles de la patte d'oie === *SA GRA - Tte ("ça gratte") *# Sartorius (ex-Couturier) *# Gracile *# Tendineux **CGT **Sartre est grat et tendre **ça gratte === Les ménisques du genou === '''''CI'''TR'''OE'''N'' (Le ménisque en forme de '''C''' est le ménisque '''I'''nterne ; le ménisque en forme de '''O''' est le ménisque '''E'''xterne) === Les obturateurs pelvi-trochantériens === Être '''ex-empt''' d''''im-pôt'''s (L'obturateur '''ex'''terne s'insère à la face '''ant'''érieure de l'os coxal, l'obturateur '''in'''terne s'insère à la face '''po'''stérieure de l'os coxal) == Mythologie == === Les 3 Grâces === '''Aglaé''' offre une u'''sine''' à Na'''thalie''' (variante : Aglaé offre Rosine à Nathalie) ** Aglaé, Euphrosine et Thalie === Les 9 Muses === Voici l'astuce des étudiants en grec à l'école pour retenir les neuf muses : '''''Cl'''ame, '''Eu'''gène, '''ta''' '''mél'''odie, '''terr'''ible '''air''' '''pol'''onais, o'''ura'''gan '''cal'''culé'' <math>\Longrightarrow</math>('''Cl'''io, '''Eu'''terpe, '''Tha'''lie, '''Mel'''pomène, '''Ter'''psichore, '''Ér'''ato, '''Pol'''ymnie, '''Ura'''nie, '''Cal'''liope) Calliope porte une couronne d’or, Clio une couronne de laurier, Érato une couronne de myrtes et de roses, Euterpe une couronne de fleurs, Melpomène une couronne de pampre de vigne, Polymnie une couronne de perles, Terpsichore une couronne de guirlandes, Thalie une couronne de lierre, et Uranie une couronne d'étoiles === Les dieux grecs === ''Hazah Phadhadah'' <math>\Longrightarrow</math>('''H'''éra, '''A'''phrodite, '''Z'''eus, '''A'''pollon, '''H'''éphaïstos, '''P'''oséidon, '''H'''ermès, '''A'''rtémis, '''D'''ionysos, '''H'''adès, '''A'''théna, '''D'''éméter, '''A'''rès, '''H'''estia) Notons que la plupart des dieux grecs commencent par la lettre "A" ou "H". {| class="wikitable" |- | Hestia || arrêta || d'aimer || Zeus<small>:</small> || possédée || <small>par</small> Dionysos<small>,</small> || <small>elle</small> erra || <small>dans</small> Athènes<small>,</small> || affolée<small>,</small> || <small>et fit</small> l'apologie || <small>de l'</small>art || effarant || d'Hermes || et d'Hadès |- | Hestia || Arès || Demeter || Zeus || Poséidon || Dionysos || Héra || Athéna || Aphrodite || Apollon || Artémis || Héphaïstos || Hermes || Hades |} === Les dieux romains === '''''J'''eune '''v'''euve '''j'''oyeuse '''c'''herche '''v'''ieux '''b'''aron '''m'''ême '''m'''alade '''a'''fin '''d'''e '''v'''ivre '''m'''ieux'' '''p'''oint <math>\Longrightarrow</math>('''J'''unon, '''V'''énus, '''J'''upiter, '''C'''érès, '''V'''ulcain, '''B'''acchus, '''M'''ercure, '''M'''inerve, '''A'''pollon, '''D'''iane, '''V'''esta, '''M'''ars, '''P'''luton) == Religion == === Les dimanches avant Pâques === Les petits protestants alsaciens (et allemands aussi, sans doute) apprenaient jadis le nom des dimanches qui précédaient Pâques grâce à la phrase : « '''I'''n '''R'''echter '''O'''rdnung '''L'''ehre '''J'''esu '''P'''assion », ce qui signifie : « Apprends dans le bon ordre la passion de Jésus ». On peut aussi dire : « '''I'''n '''R'''ektors '''O'''fen '''L'''iegen '''J'''unge '''P'''almen », ce qui veut dire : « Dans le poêle du recteur se trouvent de jeunes palmes (allusion à rameaux) ». Et ils retrouvaient : '''I'''nvocavit, '''R'''eminiscere, '''O'''culi, '''L'''aetare, '''J'''udica et '''P'''almarum (Palmsonntag - dimanche des Rameaux). === Péchés capitaux === Les initiales des [[w:sept péchés capitaux|sept péchés capitaux]] sont rassemblés dans le mot « Pô glacé ». ('''p'''aresse, '''o'''rgueil, '''g'''ourmandise, '''l'''uxure, '''a'''varice, '''c'''olère et '''e'''nvie) ** Autre expression pour retenir les 7 péchés capitaux : " CE GALOP ". (Colère, Envie, Gourmandise, Avarice, Luxure, Orgueil, Paresse) ** L' '''ENV''' ie '''EN V'''eut, lorsqu'on lui demande de travailler, la '''P'''aresse se '''P'''ousse, l' '''O'''rgueil se pense plus haut ('''O''') que les autres, la '''G'''ourmandise mange du '''G'''ateau et du '''G'''ras, la '''L'''uxure va dans un hot-'''L''', l' '''A'''varice en '''A''', la '''C'''olère '''C'''rie. **Ou encore une phrase: '''L'''es '''G'''rands '''E'''sprits '''O'''bligent '''C'''ertainement '''A''' '''P'''enser (luxure, gourmandise, envie, orgueil, colère, avarice, paresse) (entendu cité par Jacques Lacarrière lors d'une interview sur France-Culture)' ' **Une phrase courte et très facile à mémoriser, car elle ne fait pas appel qu'aux initiales qui demanderaient encore beaucoup d'efforts pour les identifier. Ici, l'utilisation de mots qui agissent immédiatement sur la divulgation des 7 péchés: ''''<nowiki>Par goût, Colette envie l'orgue luxueux d'Avarice'</nowiki>''' ('''Par'''esse, '''gou'''rmandise, '''colè'''re, '''envie''', '''orgue'''il, '''lux'''ure, '''avarice''') === Épîtres de Paul === Rococo Galéphicol ThèThè TimTim TiPhilHé ('''Ro'''mains, 1 '''Co'''rinthiens, 2 '''Co'''rinthiens, '''Gal'''ates, '''Éph'''esiens, '''Phi'''lippiens, '''Col'''ossiens, 1 '''The'''ssaloniciens, 2 '''The'''ssaloniciens, 1 '''Tim'''othée, 2 '''Tim'''othée, '''Ti'''te, '''Phil'''émon, '''Hé'''breux) === Apôtres de Jésus === '''S'''ouvent '''a'''bsent, '''J'''ean '''J'''aures '''p'''erdait '''b'''êtement '''t'''oute '''m'''onnaie le '''j'''our '''J''' de '''s'''on '''j'''eûne. '''S'''imon, '''A'''ndré, '''J'''acques, '''J'''ean, '''P'''hilippe, '''B'''arthélémy, '''T'''homas, '''M'''atthieu, '''J'''acques, '''J'''ude, '''S'''imon, '''J'''udas. '''Si JaJa Ma embarté Juju, J'enfile To Pierre''' '''Si'''mon, '''Ja'''cques, '''Ja'''cques, '''Ma'''tthieu, '''An'''dré, '''Barthé'''lémy, '''Ju'''de, '''Ju'''das, '''Jean''', '''Phil'''ippe, '''Tho'''mas, '''Pierre'''. === '''Vertus Cardinales''' === '''P'''rudence, '''J'''ustice, '''F'''orce et '''T'''empérance se retient avec : " '''P'''our '''J'''ésus, '''F'''ais '''T'''out ! ", les premières lettres des mots rappellent les quatre vertus cardinales. === Les sept sacrements catholiques === ''BECOROM'' : # Baptême # Eucharistie # Confirmation # Onction des malades # Réconciliation # Ordination # Mariage === Les sept dons de l'Esprit Saint === ''P C DS FCC (paie ces déesses fichier central des chèques)'' # Sagesse # Discernement # Conseil # Force # Connaissance # Crainte du Seigneur # Piété == Navigation == === Marine === ==== Bâbord/Tribord ==== '''''Bâ'''bord c’est g'''a'''uche, t'''r'''ibord, c’est d'''r'''oite.'' (si on regarde vers la partie avant du bateau) On peut aussi le retenir avec le mot batterie (que l’on prononce généralement « BaTri »), on a Ba à gauche (comme bâbord) et Tri à droite (comme tribord) OU plus simple avec "BaTeau". {| class="wikitable" | Bâ | Tri |- | Bâbord | Tribord |- | Gauche | Droite |} Variante : la seconde lettre de b'''â'''bord est la même que la seconde lettre de g'''a'''uche et la seconde lettre de t'''r'''ibord est la même que la seconde lettre de d'''r'''oite. Variante : Babo<s>rd ga</s>uche = Babouche. On ne retrouve qu'un A dans bAbord et gAuche et un I dans trIbord et droIte. ==== Couleur et signalisation bâbord et tribord ==== Couleur des feux de navigation d'un bateau: {| | Bâbord | '''R'''ouge |- | Tribord | '''V'''ert |} Lu de gauche à droite cela fait '''RV''' ou le prénom '''Hervé'''. ''Un marin emporte toujours Un Tricot Vert et Deux Bas Si Rouges'' = chiffres '''impairs''', '''Tri'''bord, '''Cô'''ne, '''Vert''', et chiffres '''pairs''', '''Bâ'''bord, '''Cy'''lindre, '''Rouge'''. Ou encore : en entrant au port, on trouve les balises COniques VERTES à TRIbord, et les balise CYlindrique ROUGES à BAbord. En [[w:aviron|aviron]], '''T.G.V''' à savoir '''T'''ribord-'''G'''auche-'''Vert''' car le rameur est dos à l'embarcation et donc tribord se situe à sa gauche. ==== Compartimentage ==== Le compartimentage est la méthode employée pour assurer la sécurité d'un navire contre les voies d'eau. Il consiste à diviser l'espace en compartiments étanches en-dessous de la ligne de flottaison. Il résulte de ces nombreux cloisonnements une difficulté à localiser, notamment sur les gros bâtiments, tel ou tel lieu précis (cabine, soute, etc.). Afin de régler cette difficulté se pratique un référencement des locaux selon une numérotation en quatres chiffres/lettres : la tranche (de la proue vers la poupe), le pont (de la surface vers le fond à partir du pont principal 0 situé immédiatement au-dessus de la ligne de flottaison), le rang (sous-section de tranche) et le bord (local dans un rang donné). Ex. : A216 signifie que le local se trouve dans la tranche alpha, au niveau du deuxième pont inférieur (ou faux pont), au premier rang, premier local à bâbord en partant de la ligne médiane (ou de la coursive centrale) du navire. La mémorisation de l'ordre des bords s'effctue avec la phrase mnémotechnique suivante : "j'aime les femmes en slip", le nombre de lettres de ces mots donnant la suite 1-5-3 (impairs sur tribord) 6-2-4 (pairs sur bâbord). ==== Nœuds ==== La phrase « ''Le serpent sort du trou, tourne autour de l'arbre, et rentre dans le trou'' » permet de se rappeler de la méthode pour faire un [[w:nœud de chaise|nœud de chaise]]. === Aviation === Chez les pilotes d’avions utilisant le système [[w:Precision_Approach_Path_Indicator|Vasi]] pour trouver l’altitude correcte à l’atterrissage en [[w:vol à vue|vol à vue]], des panneaux rouges et blancs à côté de la piste leur fournissent de précieuses indications qui ont abouti à cette comptine : :« ''White over white, you're high as a kite'' :''Red over white, you're right'' :''Red over red, you're dead'' » (Blanc sur blanc : trop haut, rouge sur blanc : correct, rouge sur rouge : trop bas). Pour récolter toutes les informations nécessaires pour compléter leur feuille de route, les pilotes aguerris utilisent la phrase : '''''R'''''etranchez '''v'''otre '''d'''érive, '''c'''ela '''v'''ous '''d'''onne '''c'''haque '''m'''esure '''d'''u '''c'''ap '''c'''ompas. Route vraie - X (dérive) / Cap vrai - Déclinaison / Cap magnétique - déviation / Cap compas Pour le décollage : CC PP VV TT (ou 3T) Compas, Conservateur de cap (ou gyro compas), Phares, Pompe, Volets, Verrière, [[w:Transpondeur|Transpondeur]], Top, (Talons au sol). Cela dépend bien sûr du type d'avion. Pour l’observateur au sol et face à l’avion, contrôleur, mécanicien de piste, le rouge est à droite et le vert est à gauche. Très pratique la nuit, on ne voit rien d’autre que les feux. Moyen mnémotechnique : Quand on se sert du vin à boire, on a le "rouge" dans la main droite et le "verre" dans la main gauche. Ou plus facile à retenir : Comme en politique, le rouge est toujours à gauche ! == Vie quotidienne == La vie quotidienne regorge de moyens mnémotechniques plus ou moins utiles. === Droite et Gauche === Se souvenir de sa main habile (droitier ou gaucher) Utiliser les initiales en majuscules (G et D) : l'arrondi est du côté correspondant (à Gauche pour G), (à droite pour D). En anglais, pour Left et Right (Gauche et Droite). On peut faire un L avec la main gauche, avec le pouce et l'index et non avec la main droite. D'où Left=gauche et right=droite. === Mois courts et mois longs === En mettant ses poings fermés côte à côte, les bosses des [[w:phalange|phalange]]s peuvent correspondre aux mois de 31 jours du [[w:calendrier|calendrier]] et les creux entre chacune aux mois de 30 jours ou moins. Et sans compter la jonction entre les mains comme un creux. On peut aussi le faire avec une seule main (c'est mieux pour les enfants, car ça permet de suivre le décompte des mois avec un doigt de l'autre main...) : on commence sur le premier sommet pour janvier, on s'arrête sur le dernier sommet pour juillet et, pour les mois suivants, on repart en arrière en comptant de nouveau le sommet (ou bien, on recommence au sommet initial, pour bien marquer les 2 mois de 31 jours). Attention ! Juillet-Aout ont 31 Jours mais Décembre-Janvier aussi !! === Heure d’été, heure d’hiver === En été on avance d’une heure, car "'''é'''té" et "'''a'''vance" commencent par une voyelle. En été le soleil qui passe à travers les volets nous réveille tôt car on dort une heure en moins. En hiver on recule d’une heure, car "'''h'''iver" et "'''r'''ecule" commencent par une consonne. En hiver on hiberne, donc on dort une heure en plus. Le changement d’heure se faisant en avril (!) et en octobre : * OCTOBRE finit par RE donc on REcule * ''AVRIL commence par AV donc on AVance'' Hélas..... cette information est affichée sur de très nombreux sites.. alors que le changement annoncé en AVRIL.. est en réalité réalisé le dernier dimanche de MARS ! Comme le changement a lieu en MARS et OCTOBRE, et qu'en général on ne peut pas (ou ne doit pas) faire reculer une aiguille sur une montre analogique : * MARS étant plus court (4 lettres), on avance de 1 heure, * OCTOBRE étant plus long (7 lettres), on avance de 23 heures (ou 11 heures pour les montres et horloges analogiques sans date). Pour les anglophones : Spring Forward, Fall Back (Spring étant le Printemps et Fall l'Automne)... === Retrouver de tête le nom du jour de la semaine quelle que soit la date donnée === {{pas clair}} Par convention, on associe les chiffres aux lettres suivantes : <pre> 0 = S, Z 1 = T, D 2 = N, Gn 3 = M 4 = R 5 = L, Y, ill 6 = CH, J, Ge 7 = K, Qu, Gu 8 = F, Ph, V 9 = P, B </pre> ==== N°1 - liens entre jours et lettres : ==== Lundi est le premier jour de la semaine . Donc '''Lundi = 1''' mardi = 2 ... ==== N°2 - liens entre mois et lettres ==== ===== a) correspondances pour année normale ===== : ** Janvier Février Mars... deviennent "'''S'''a'''M''' '''M'''e '''J'''e'''T'''e'''R'''a a'''G'''e'''N'''ou'''iLL'''é '''S'''on '''M'''a'''iLL'''ot" '''S''' = Janvier ; '''M''' = Février ; '''M''' = Mars '''J''' = Avril ; '''T''' = Mai ; '''R''' = Juin '''g'''= Juillet ; '''n'''= Août ; '''L'''= Septembre '''s'''= Octobre ; '''m'''= Novembre ; '''L'''= Décembre ===== b) correspondances pour année bissextile ===== "'''G'''i'''N'''o '''M'''e '''J'''e'''T'''e'''R'''a ..." '''G''' = Janvier ; '''N''' = Février ; etc ===== N°3 - facteur en fonction du siècle ===== ====== a) Avant le 4 octobre 1582, enlever 7 au siècle (''' -7''' ) ====== Année 670 : siècle 6 '''-7''' = 0 Année 1100 : siècle 11 '''-7''' = 4 Et le résultat on le transforme : 4 devient 0 , 3 devient 1 , 5 devient 6 , 2 reste 2 et inversement. Moyen mnémotechnique (suivant la convention) : RuSé MaTou :: 4-0 3-1 NoNNe LouChe :: 2-2 5-6 ====== b) À compter du 4 Octobre 1582 ====== soit le '''4''' - '''10''' - '''1582''' , c'est à dire le jour où le '''R'''oi '''T'''hé'''S'''ée '''T'''é'''L'''é'''PH'''o'''N'''a , on enlève 4 et non 7 , autant de multiple possible : **Année 1800 : siècle 18 - ( '''4'''x4 ) = 2 **Année 2000 : siècle 20 - ( '''4'''x5 ) = 0 La transformation donne 0-6 1-4 2-2 3-0 {| class="wikitable" |- | 0 || 1 || 2 || 3 |- | '''6''' || '''4''' || '''2'''|| '''0''' |- | '''CH'''è... || ...'''r'''e || '''N'''iai... || '''se''' |- | 0-6 || 4-1 || 2-2 || 0-3 |} ==== Calcul pour le 26 septembre 1955 ==== Il y a plusieurs opérations à réaliser en se conformant aux règles citées . **26 Sept 1955 règle 2a :: septembre = L = ''5'' **26+''5'' = 31 règle 3b ** 31-(7x4) = '''''3''''' **55 - (7x4) = 55-28= 27 **27 + (27/4) = 27+6 = 33 **33 - (7x4 ) = 33-28= '''''5''''' ** '''''5 + 3 ''''' =''''' 8''''' comme il y a 7 jours dans la semaine : *''''' 8-7 '''''= '''1''' le 1 correspond à '''lundi''' donc le 26 septembre 1955 était un '''lundi''', toute la journée !!! ==== Sinon pour l'année cours, passée ou à venir, ==== le moyen le plus simple et le plus rapide est de diviser l'année en trimestres . Pour chaque mois, on cherche le quantième du premier dimanche par exemple . On fait une phrase par trimestre et pour trouver la correspondance on rajoute 7 + de 1 à 6 . Exemple, pour 2012, {| class="wikitable" |- ! Janvier !! Février !! Mars |- | '''1''' || '''5''' || '''4''' |- | '''D'''e || '''<nowiki>L'</nowiki>''' || ai'''R''' |} Donc, le premier dimanche de Janvier 2012 est le 1er janvier . Pour aller à mon Rendez vous du 3 je rajoute 2. ** Selon ma convention déjà vue , lundi=1 mardi=2 mercredi=3 jeudi=4 vendredi=5 samedi=6 et dimanche=7 . Alors 2 correspond à mardi donc le 3 Janvier 2012 est mardi pour aller au 18 janvier 2012 , une semaine ayant 7 jours, même sous le règne actuel, je retire autant de semaine complète que possible . Donc 18 - (2x7 ) = 18 - 14 = 4 . Surprise, le 18 janvier 2012 sera un mercredi . Soit parce que j'ai rajouté 3 jours au dimanche, soit parce que j'ai décidé une fois pour toutes que le dimanche est le premier jour de la semaine, donc le mardi le 2ème etc ... ** dans ce cas, dimanche=1 mardi=2 mercredi=4 jeudi=5 vendredi=6 samedi=7 dimanche=8-7=1 C'est selon sa préférence intellectuelle . Je vous laisse continuer pour février et les autres trimestres. {| class="wikitable" |- ! janvier !! février !! mars !! avril !! mai !! juin !! juillet !! aout !! sept !! oct !! nov !! déc |- | 1 || 5 || 4 || 1 || 6 || 3 || 1 || 5 || 2 || 7 || 4 || 2 |- | D || l || r || t || ch || m || d || l || n || k || r || g |- | de || l' || air || tu || chô || me || dans || la || nuit || qui || rè || gne |} === Valeur d'un Euro en [[w:Franc français|Francs français]] === Selon le même principe que pour les décimales de ''π'' : {| border="0" cellpadding="0" cellspacing="1" |align="center"|''Chacun'' |&nbsp; |align="center"|''saura'' |&nbsp; |align="center"|''enfin'' |&nbsp; |align="center"|''convertir'' |&nbsp; |align="center"|''notre'' |&nbsp; |align="center"|''monnaie'' |- !align="center"|6 !align="center"|, !align="center"|5 | !align="center"|5 | !align="center"|9 | !align="center"|5 | !align="center"|7 |} === Morse === Le code morse est facilement mémorisable à l’aide des codes courts et longs remplacés par des syllabes. Le code long (-) remplacé par une syllabe en "O". Le code court (.) remplacé par une des autres voyelles. Ex : A = .- = Au/tO (une syllabe en A pour le . et une syllabe en O pour le -) La liste complète est [[w:Alphabet Morse#Tableau Mn.C3.A9motechnique|Ici]] === Les vins === ==== AOC de la côte de Nuits ==== Un mnémonique permettant de se rappeler des [[wikipedia:AOC|AOC]] communales de la [[wikipedia:Côte de Nuits|Côte de Nuits]], et dans l'ordre géographique en plus, par Paul Brunet, auteur du livre ''Le vin et les vins au restaurant'' : ''Messieurs, faites gaffe, mon chat vous voit noir'' (Marsannay, Fixin, Gevrey-Chambertin, Morey-Saint-Denis, Chambolle-Musigny, Vougeot, Vosne-Romanée, Nuits-Saint-Georges). ==== Nom des bouteilles de vin ==== Ce moyen mnémotechnique permet de mémoriser les principales tailles de [[w:bouteille de vin|bouteilles]] dans l'ordre croissant de contenance : * Car de bon matin je remarquais mal sa banalité naturelle (quart, demi, bouteille, magnum, jéroboam, réhoboam, mathusalem, salmanazar, balthazar, nabuchodonosor). Autre phrase mnémo, plus complète : * PICARD FIT : DE BON MATIN, JE REMARQUE SA BANALITÉ, SA MATIERE SI PAUVRE. Pour se rappeler tous les contenants de vin ou de champagne : * PIccolo, QUARt, FIllette, DEmi-bouteille, Bouteille, MAgnum, Jeroboam, REhoboam, Mathusalem, SAlmanazar, Balthazar, Nabuchodonosor, SAlomon, Melchisédech, Souverain, Primat. === <u>Dresser une table</u> === Pour se souvenir d'où mettre la fourchette et le couteau : Four'''<u>ch</u>'''ette à gau'''<u>ch</u>'''e, couteau à droite. ou encore : A, B, '''C''', '''D''' ... '''C'''outeau à '''D'''roite. E, '''F''', '''G''', H ... '''F'''ourchette à '''G'''auche == Cinéma, Bande dessinée... == === Dupond et Dupont === Pour différencier les deux [[w:Dupond et Dupont|dupondt]] de la bande dessinée [[w:Les Aventures de Tintin et Milou|Les Aventures de Tintin et Milou]], celui à la moustache tombante comme un D est Dupond, celui à la moustache pointue comme les barres du T est Dupont. === Les 7 nains === '''A''' '''J'''ouer '''P'''resque '''S'''eul '''T'''u '''D'''eviens '''G'''rincheux * Atchoum, Joyeux, Prof, Simplet, Timide, Dormeur, Grincheux : les 7 nains dans Blanche Neige... === Les Simpson === Pour différencier les deux sœurs de Marge, on observe les cheveux. Patty n'a pas de raie au centre et Selma a les cheveux découpés en deux blocs par une raie ou on regarde les boucles d'oreilles qui sont différentes Sinon, regarder les boucles d'oreilles de Patty, elles sont triangulaires (P comme Pythagore). == Sport == === Escalade === ''Pour la prochaine longueur je reste en bas, donc je me vache au plus bas.'' <br> :En escalade, permet de savoir sur quel point d'assurage se vacher lors du relais '''réversible''' uniquement. == Voir aussi == === Article connexe === * [[w:Code chiffres-sons|Code chiffres-sons]] === Liens externes === * [https://jeretiens.net -JeRetiens.net- Libre recueil ayant pour objectif de rassembler tous les trucs et astuces mnémotechniques pour retenir et apprendre plus facilement.] * [http://www.finallyover.com/categorie-1079667.html Méthodes thématiques de mémorisation] * [http://www.echolalie.org/wiki/index.php?ListeMnemotechnique Liste mnémotechnique] * [http://trucsmaths.free.fr/Pi.htm#poeme Le nombre pi] * [http://www.francaisfacile.com/exercices/exercice-francais-2/exercice-francais-75628.php francaisfacile.com] === Références === <references /> [[Catégorie:minilivres]] l62ze4ilh2jks56i93w7n3rhlax43nm 772810 772809 2026-09-22T07:52:19Z CentreMersenne 124596 Annulation de la modification [[Special:Diff/772809|772809]] de [[Special:Contributions/CentreMersenne|CentreMersenne]] ([[User talk:CentreMersenne|discussion]]) 772810 wikitext text/x-wiki Cette page contient une '''liste de [[w:mnémotechnique|mnémotechniques]]''', c’est-à-dire différentes constructions qui facilitent la mémorisation. Par exemple : afin de retenir beaucoup plus facilement les sept péchés capitaux, une phrase mnémotechnique possible offrant une image plus visuelle regroupant une partie ou la totalité d'un péché : Par goût, Colette envie l'orgue luxueux d'Avarice (Paresse, gourmandise, colère, envie, orgueil, luxure, avarice) == Atmosphère == === Structure verticale === L’atmosphère terrestre présente une structure verticale en couches basée sur l’évolution de la température. On distingue la Troposphère (siège des phénomènes météorologiques), la Stratosphère, la Mésosphère et la Thermosphère (et l'exosphère). '''T'''out '''S'''ur '''M'''a '''T'''ête '''Trop''' de '''Str'''ess et de '''Més'''aventures '''Ter'''rifient à l''''Ex'''trême == Mathématiques == === Les 126 premières décimales du nombre π (pi) === Le nombre de lettres de chaque mot de ce poème correspond à une décimale de [[w:Pi|Pi]]. Un mot de dix lettres correspond au chiffre 0. {| |- align="center" | ''Que'' || || ''j'' || ''<nowiki>’</nowiki>'' || ''aime'' || ''à'' || ''faire'' || ''apprendre'' || ''un'' || ''nombre'' || ''utile'' || ''aux'' || ''sages'' || ''!'' |- align="center" ! 3 !! , !! 1 !! !! 4 !! 1 !! 5 !! 9 !! 2 !! 6 !! 5 !! 3 !! 5 |} {| |- align="center" | ''Immortel'' || ''Archimède'' || '','' || ''artiste'' || ''ingénieur'' || '','' |- align="center" ! 8 !! 9 !! !! 7 !! 9 |} {| |- align="center" | ''Qui'' || ''de'' || ''ton'' || ''jugement'' || ''peut'' || ''priser'' || ''la'' || ''valeur'' || ''?'' |- align="center" ! 3 !! 2 !! 3 !! 8 !! 4 !! 6 !! 2 !! 6 |} {| |- align="center" | ''Pour'' || ''moi'' || '','' || ''ton'' || ''problème'' || ''eut'' || ''de'' || ''pareils'' || ''avantages'' || ''...'' |- align="center" ! 4 !! 3 !! !!3!! 8 !! 3 !! 2 !! 7 !! 9 |} {| |- align="center" | ''Jadis'' || '','' || ''mystérieux'' || , || ''un'' || ''problème'' || ''bloquait'' |- align="center" ! 5 !! !! 0 !! !! 2 !! 8 !! 8 |} {| |- align="center" | ''Tout'' || ''l'' || ''<nowiki>’</nowiki>'' || ''admirable'' || ''procédé'' || '','' || ''l'' || ''<nowiki>’</nowiki>'' || ''œuvre'' || ''grandiose'' |- align="center" ! 4 !! 1 !! !! 9 !! 7 !! !! 1 !! !! 6 !! 9 |} {| |- align="center" | ''Que'' || ''Pythagore'' || ''découvrit'' || ''aux'' || ''anciens'' || ''Grecs'' ||''.'' |- align="center" ! 3 !! 9 !! 9 !! 3 !! 7 !! 5 |} {| |- align="center" | ''Ô'' || ''quadrature'' || ''!'' || ''Vieux'' || ''tourment'' || ''du'' || ''philosophe'' || ''...'' |- align="center" ! 1 !! 0 !! !! 5 !! 8 !! 2 !! 0 |} {| |- align="center" | ''Insoluble'' || ''rondeur'' || '','' || ''trop'' || ''longtemps'' || ''vous'' || ''avez'' |- align="center" ! 9 !! 7 !! !! 4 !! 9 !! 4 !! 4 |} {| |- align="center" | ''Défié'' || ''Pythagore'' || ''et'' || ''ses'' || ''imitateurs'' || ''.'' |- align="center" ! 5 !! 9 !! 2 !! 3 !! 0 |} {| |- align="center" | ''Comment'' || ''intégrer'' || ''l'' || ''<nowiki>’</nowiki>'' || ''espace'' || ''plan'' || ''circulaire'' || ''?'' |- align="center" ! 7 !! 8 !! 1 !! !! 6 !! 4 !! 0 |} {| |- align="center" | ''Former'' || ''un'' || ''triangle'' || ''auquel'' || ''il'' || ''équivaudra'' || ''?'' |- align="center" ! 6 !! 2 !! 8 !! 6 !! 2 !! 0 |} {| |- align="center" | ''Nouvelle'' || ''invention'' || '':'' || ''Archimède'' || ''inscrira'' |- align="center" ! 8 !! 9 !! !! 9 !! 8 |} {| |- align="center" | ''Dedans'' || ''un'' || ''hexagone'' || '';'' || ''appréciera'' || ''son'' || ''aire'' |- align="center" ! 6 !! 2 !! 8 !! !! 0 !! 3 !! 4 |} {| |- align="center" |''Fonction'' || ''du'' || ''rayon'' || ''.'' || ''Pas'' || ''trop'' || ''ne'' || ''s'' || ''<nowiki>’</nowiki>'' || ''y'' || ''tiendra'' |- align="center" ! 8 !! 2 !! 5 !! !! 3 !! 4 !!2 !! 1 !! !! 1 !! 7 |} {| |- align="center" |''Dédoublera'' || ''chaque'' || ''élément'' || ''antérieur'' |- align="center" ! 0 !! 6 !! 7 !! 9 |} {| |- align="center" |''Toujours'' || ''de'' || ''l'' || ''<nowiki>’</nowiki>'' || ''orbe'' || ''calculé'''e'''''¹ || ''approchera'' |- align="center" ! 8 !! 2 !! 1 !! !! 4 !! 8 !! 0 |} {| |- align="center" |''Définira'' || ''limite'' || '';'' || ''enfin'' || '','' || ''l'' || ''<nowiki>’</nowiki>'' || ''arc'' || '','' || ''le'' || ''limiteur'' |- align="center" ! 8 !! 6 !! !! 5 !! !! 1 !! !! 3 !! !! 2 !! 8 |} {| |- align="center" |''De'' || ''cet'' || ''inquiétant'' || ''cercle'' || '','' || ''ennemi'' || ''trop'' || ''rebelle'' |- align="center" ! 2 !! 3 !! 0 !! 6 !! !! 6 !! 4 !! 7 |} {| |- align="center" |''Professeur'', || ''enseignez'' || ''son'' || ''problème'' || ''avec'' || ''zèle'' || ''!'' |- align="center" ! 0 !! 9 !! 3 !! 8 !! 4 !! 4 |} ¹ Le mot orbe est du masculin mais ce ne fut pas toujours le cas, ceci induit à présent une faute d’accord à « ''calculée'' » que l’on peut remplacer par « ''escompté'' » pour conserver le bon nombre de lettres. === Premières décimales de l’inverse du nombre π : 1/π === La valeur de '''1/π = 0,3183098''' se retient sous la forme d’une phrase historique faisant référence aux trois glorieuses : {{Citation|« Les 3 journées de 1830 ont renversé 89 [la Révolution de 1789] »}}. Le score de la finale de la coupe du monde de football 98 a fait '''un surpris''' (1 sur pi), côté Brésil : '''0''', '''3''' (c’est le feu ! = '''18''' = '''''Cher''''' payé ?) ; '''3-0''' ('''''Gard'''''ez en souvenir) '''98''' === Trigonométrie === Le principe est de ne retenir que la première lettre ou la première syllabe des mots-clés de chaque définition ou théorème : ==== Définitions ==== * « Cosinus = côté Adjacent sur l'Hypoténuse » * « Sinus = côté Opposé sur l'Hypoténuse » * « Tangente = côté Opposé sur côté Adjacent » Une "phrase" permet de se rappeler ces trois définitions à la fois : '''cah soh toa''' pour « ''casse-toi'' » : '''C'''osinus = '''A'''djacent sur '''H'''ypoténuse ; '''S'''inus = '''O'''pposé sur '''H'''ypoténuse ; '''T'''angente = '''O'''pposé sur '''A'''djacent. Certains préfèrent '''soh cah toa'''. On peut aussi ressortir les dénominateurs de chaque fraction (afin de ne pas mélanger numérateurs et dénominateurs dans ces égalités) en apprenant les sons : '''SO-CA-TO, H-H-A''' (HHA étant les dénominateurs : '''S'''in ='''O'''pp /''H''yp , '''C'''os = '''A'''dj/''H''yp, '''T'''an='''O'''pp/''A''dj) <br /> D'autres méthodes consistent à associer un "mot" facile à retenir à chacune des trois définitions:<br /> - Cosadi - Sinopi - Tanopad <br /> - cosadjip - sinopip - tangopaj <br /> - CAHier - SOHo - TOAst (ou COCA) ==== Théorèmes ==== * « sin (a+b) = sin a cos b + cos a sin b » devient « ''sico cosi'' » * « cos (a+b) = cos a cos b - sin a sin b » devient « ''coco sisi'' » (ou « ''coco MOINS sisi'' ou « ''coco ISsi'' » pour retenir le signe) * À noter que la formule « ''sico cosi / coco moins sisi'' » ou « ''Coco si méchant, si, Coco, si'' » permet également d’apprendre les formules de factorisation suivantes : sin p + sin q = 2 sin [(p+q)/2] •cos [(p-q)/2] sin p - sin q = 2 cos [(p+q)/2] •sin [(p-q)/2] cos p + cos q = 2 cos [(p+q)/2] •cos [(p-q)/2] cos p - cos q = -2 sin [(p+q)/2] •sin [(p-q)/2] Avec p = A + B et q = A - B '''Cosinus est menteur et raciste''' ('''CO'''s comme '''CO'''n) en effet cos (a+b) donne (cos a cos b) - (sin a sin b). Cosinus est donc menteur puisque le signe de l’addition (positive) est négatif. Cosinus est raciste puisque on obtient (cos a cos b) d’une part et (sin a sin b) d’autre part : les cosinus et les sinus ne se mélangent pas. '''co'''sinus est un '''co'''pain '''co'''n (copain pour le sens et con pour le signe): cos(a'''+'''b)= cos(a)cos(b) '''-''' sin(a)sin(b) : les cosinus restent ensemble, mais le signe change. '''s'''inus est une '''s'''alade '''s'''ympa (salade pour le sens et sympa pour le signe): sin (a'''+'''b)=sin(a)cos(b) '''+''' sin(b)cos(a) : sin et cos se mélangent mais le signe reste le même On trouve également : '''opip adjip opadj''' : sinus ('''op'''posé sur '''hyp'''oténuse), cosinus ('''adj'''acent sur '''hyp'''oténuse), tangente ('''op'''posé sur '''adj'''acent). La phrase prononcée rapidement d’un seul coup est très facile à mémoriser. De même que '''SOH CAH TOA''': '''S'''inus= '''O'''pposé sur '''H'''ypoténuse '''C'''osinus= '''A'''djacent sur '''H'''ypoténuse '''T'''angente= '''O'''pposé sur '''A'''djacent On peut lui substituer la formule plus percutante : '''CAH SOH TOA''' (à prononcer Casse toi ! ) === Dates et constantes === Le [[w:code chiffres-sons|code chiffres-sons]] est une méthode qui permet de se souvenir de dates ou de valeurs numériques en formant des phrases. === Formules de géométrie === * Circonférence d’un cercle : 2 pi R (2 pierres) ** La circonférence est toute fière d’être égale à 2 pi R * Aire d’un disque: pi R<sup>2</sup> (« pierre carrée » ou « pierre deux ») ** Le cercle est tout joyeux d’être égal à pi R<sup>2</sup> (prononcer « pi R deux ») * « Le volume de la sphère, est quoi qu’on y puisse faire, 4/3 pi R<sup>3</sup>, fut-elle de bois. » ([[w:fr:Marcel Pagnol|Marcel Pagnol]]) Le volume d'une sphère, qu'elle soit de pierre, qu'elle soit de bois est égal aux 4/3 de pi R3 * Le volume d'une pizza (d'un camembert, ou de n'importe quel objet semblable) de rayon 'z' et de hauteur 'a' est égale à '''Pi.(z.z).a''' (la formule correspond à son nom) soit V = π.z<sup>2</sup>.a === Analyse vectorielle === [[File:DRG chart fr.svg|thumb|right|300px|Diagramme des principales relations entre opérateurs de calcul vectoriel.]] * Opérateurs s'annulant: '''DiR'''i'''G'''é (décrivant les flèches centrales sur le diagramme à droite) ** '''DiR'''i: <math>\mathrm{div}(\overrightarrow{\mathrm{rot}})=0</math> ** '''R'''i'''G''': <math>\overrightarrow{\mathrm{rot}}(\overrightarrow{\mathrm{grad}})=\vec{0}</math> * Autres formules (flèches reliant div et grad sur le diagramme à droite): ** <math>\Delta = \mathrm{div}(\overrightarrow{\mathrm{grad}})</math> ** <math>\overrightarrow{\mathrm{grad}}(\mathrm{div})= \overrightarrow{\mathrm{rot}}(\overrightarrow{\mathrm{rot}})+\vec{\Delta}</math> === Ordre des opérations === En algèbre, les opérations simples : <code>(</code>&nbsp;<code>)</code>, <code>+</code>, <code>-</code>, <code>&times;</code> et <code>&divide;</code>, sont évaluées selon un certain ordre : '''PEMDAS''' pour « '''p'''arenthèses, '''e'''xposant, '''m'''ultiplication, '''d'''ivision, '''a'''ddition et '''s'''oustraction ». Pour plus de détails sur l'application de ce mnémonique, voir [[:w:fr:Ordre des opérations|Ordre des opérations]]. === Double distributivité === Retenir le mot « '''PIED''' » qui donne les termes à regrouper lorsque l’on développe : '''P'''remiers, '''I'''ntérieurs, '''E'''xtérieurs, '''D'''erniers. === Constante e<ref name="villemin.gerard">http://villemin.gerard.free.fr/Wwwgvmm/MnemoTe/Phrase.htm</ref> === {| border="0" cellpadding="0" cellspacing="1" |align="center"|Tu | &nbsp; |align="center"|aideras | &nbsp; |align="center"|à | &nbsp; |align="center"|rappeler | &nbsp; |align="center"|ta | &nbsp; |align="center"|quantité | &nbsp; |align="center"|à | &nbsp; |align="center"|beaucoup | &nbsp; |align="center"|de | &nbsp; |align="center"|docteurs | &nbsp; |align="center"|amis. |- |align="center"|2 | ,&nbsp; |align="center"|7 | &nbsp; |align="center"|1 | &nbsp; |align="center"|8 | &nbsp; | align="center" |2 | &nbsp; |align="center"|8 | &nbsp; |align="center"|1 | &nbsp; |align="center"|8 | &nbsp; |align="center"|2 | &nbsp; |align="center"|8 | &nbsp; |align="center"|4 |} === Nombre d'or<ref name="villemin.gerard"/> === {| border="0" cellpadding="0" cellspacing="1" |align="center"|Ô | &nbsp; |align="center"|nombre | &nbsp; |align="center"|d' | &nbsp; |align="center"|élégance | &nbsp; |align="center"|! | &nbsp; |align="center"|Toi, | &nbsp; |align="center"|toi, | &nbsp; |align="center"|grandiose, | &nbsp; |align="center"|étonnant : | &nbsp; |align="center"|''le nombre d'or''. |- |align="center"|1 | ,&nbsp; |align="center"|6 | &nbsp; |align="center"|1 | &nbsp; |align="center"|8 | &nbsp; |align="center"|0 | &nbsp; |align="center"|3 | &nbsp; |align="center"|3 | &nbsp; |align="center"|9 | &nbsp; |align="center"|8 | &nbsp; |align="center"| &nbsp; |} (! pour 0) === Statistiques === * [[w:Règle 68-95-99.7|Règle 68-95-99.7]] : la proportion des échantillons entre [-σ, +σ], [-2σ, +2σ], [-3σ, +3σ] pour une distribution gaussienne centrée. * Erreurs de première espèce et deuxième espèce. ** Se rappeler la fable d’Ésope dans laquelle un enfant [[wikt:crier au loup|crie au loup]] (hypothèse nulle <math>H_0</math>: « il n'y a aucun loup »). **# D'abord, les villageois pensent qu'il y a un loup alors qu'il n'y en a aucun (erreur de première espèce). **# Puis, les villageois pensent qu'il n'y a aucun loup alors qu'il y en a un (erreur de seconde espèce). ** Il y a une barre dans '''P'''ositif (faux positif : erreur de type '''I''') et deux barres dans '''N'''égatif (faux négatif : erreur de type '''II'''). == Sciences == === Astronomie === ==== Ordre des planètes du Système solaire ==== Il existe toute une série de termes mnémotechniques pour se souvenir de l'ordre des planètes à l’intérieur du [[w:système solaire|Système solaire]]. La première lettre de chaque mot de cette phrase correspond à la première lettre de chaque [[w:planète|planète]], de la plus rapprochée à la plus éloignée du Soleil. L'[[w:apostrophe|apostrophe]] ou la [[w:virgule|virgule]] peut représenter la [[w:ceinture d'astéroïdes|ceinture d'astéroïdes]] entre [[w:Mars (planète)|Mars]] et [[w:Jupiter|Jupiter]]. Voici l’ordre des planètes du Système solaire : Mercure, Vénus, Terre, Mars, Jupiter, Saturne, Uranus, Neptune. ''À NOTER que selon la [[w:Définition des planètes de l'UAI|nouvelle définition]] de l’[[w:Union astronomique internationale|Union astronomique internationale]] d’août [[w:2006|2006]], [[w:Pluton (planète naine)|Pluton]] n’est plus considérée comme une [[w:planète|planète]] mais comme une planète naine ([[w:(134340) Pluton|(134340) Pluton]])'' (de même que [[w:(1) Cérès|(1) Cérès]], [[w:(136199) Éris|(136199) Éris]], [[w:(136108) Haumea|(136108) Haumea]] et [[w:(136472) Makemake|(136472) Makemake]]), <br/> '''''Ordre des planètes du Système solaire : '''Mercure, Vénus, Terre, Mars, Jupiter, Saturne, Uranus, Neptune.'' On emploie par exemple les phrases suivantes : *'''''M'''ême '''V'''ieux '''T'''ruc '''M'''ais '''J''''en '''S'''ais '''U'''n '''N'''ouveau.'' *'''''M'''a '''V'''ieille '''T'''ante '''M'''arie a '''J'''eté '''S'''amedi '''U'''n '''N'''avet.'' *'''''M'''a '''V'''ieille '''T'''rompette '''M'''e '''J'''oue '''S'''on '''U'''ltime '''N'''octurne.'' *'''''M'''a '''V'''oiture '''T'''e '''M'''ène '''J'''oyeusement '''S'''ur '''U'''ne '''N'''ationale.'' *'''''M'''arie, '''V'''iendras-'''T'''u '''M'''anger '''J'''eudi '''S'''ur '''U'''ne '''N'''appe ?'' *'''''M'''angez '''V'''os '''T'''artes, '''M'''ais '''J'''uste '''S'''ur '''U'''ne '''N'''appe .'' *'''''M'''e '''V'''oici '''T'''oute '''M'''ignonne''',''' '''J'''e '''S'''uis '''U'''ne '''N'''ébuleuse.'' *'''''M'''e '''V'''oici '''T'''oute '''M'''odifiée''',''' '''J'''e '''S'''uis '''U'''ne '''N'''ouveauté.'' *'''''M'''e '''V'''oilà '''T'''out '''M'''ouillé''',''' '''J''''ai '''S'''uivi '''U'''n '''N'''uage.'' *'''''M'''e '''V'''oilà '''T'''oute '''M'''ouillée''',''' '''J'''e '''S'''uis '''U'''ne '''N'''ymphomane.'' *'''''M'''on '''V'''ieux, '''T'''u '''M''''as '''J'''eté '''S'''ur '''U'''ne '''N'''avette.'' *'''''M'''on '''V'''ioloncelle '''T'''ombe, '''M'''ais '''J'''e '''S'''auve '''U'''ne '''N'''ote. *'''''M'''aman '''V'''ole '''T'''ous '''M'''es '''J'''ouets, '''S'''auf '''U'''n '''N'''ounours !'' *'''''M'''e '''V'''oici, '''T'''onton '''M'''arcel, '''J'''e '''S'''uis '''U'''n '''N'''ageur.'' *'''''M'''a '''V'''ille '''T'''hionville '''M'''ontre '''J'''oyeusement '''S'''on '''U'''nivers '''N'''octurne.'' *'''''M'''on '''V'''élo '''T'''e '''M'''ènera ''' J'''usque '''S'''ur '''U'''n '''N'''uage.'' *'''''M'''on '''V'''élo '''T'''ourne '''M'''al, ''' J''''en '''S'''ouhaite '''U'''n '''N'''ouveau.'' *'''''M'''onsieur, '''V'''ous '''T'''ravaillez '''M'''al ; - ''' J'''e '''S'''uis '''U'''n '''N'''ovice.'' *'''''M'''al '''V'''êtu '''T'''oi '''M'''ême, '''J'''e '''S'''uis '''U'''n '''N'''udiste'' *'''''M'''on '''V'''ieux '''T'''outou '''M'''édor '''J'''oue '''S'''ur '''U'''n '''N'''uage'' *'''''M'''a '''V'''erge '''T'''e '''M'''ènera '''J'''usque '''S'''ur '''U'''n '''N'''uage'' *'''''M'''ais '''V'''ous '''T'''ombez '''M'''al, '''J''''ai '''S'''auté '''U'''ne '''N'''aine'' * '''''M'''essieurs!''' V'''otre '''T'''rahison '''M'''<nowiki/><nowiki>'écœure: </nowiki>'''J'''ouer''' S'''ur '''U'''ne '''N'''omenclature!'' (au sujet de la disparition de Pluton de la liste) *'''''S'''ors (pour Soleil) -'''M'''oi '''V'''ite '''T'''a '''M'''armite '''J'''aune '''S'''ur '''U'''ne '''N'''appe.'' *'''''M'''arquez '''V'''otre '''T'''emps '''M'''esuré '''J'''uste '''S'''ous '''U'''ne '''N'''anoseconde.'' '''Et pour les nostalgiques de Pluton...''' * '''''M'''anon '''V'''iendras '''T'''u '''M'''anger '''J'''eudi '''S'''ur '''U'''ne '''N'''appe '''P'''ropre.'' *'''''M'''ercure '''V'''eut '''T'''aquiner '''M'''ars, '''J'''e '''S'''uis '''U'''ne '''N'''ouvelle '''P'''lanète.'' *'''''M'''ademoiselle, '''V'''ous '''T'''ravaillez '''M'''al, '''J'''e '''S'''uis '''U'''n '''N'''ouveau '''P'''rofesseur'' *''Le '''M'''onde '''V'''oit '''T'''ourner du '''M'''atin '''J'''usqu'au '''S'''oir '''U'''niquement '''N'''euf '''P'''lanètes''. *'''''M'''on '''V'''ieux, '''T'''u '''M'''e '''J'''ettes '''S'''ur '''U'''ne '''N'''ouvelle '''P'''lanète.'' *'''''M'''onsieur '''V'''euillez '''T'''ournez '''M'''a '''J'''upe '''S'''ans '''U'''ne '''N'''aïve '''P'''udeur.'' *'''''M'''e '''V'''oici, '''T'''oute '''M'''ignonne, '''J'''e '''S'''uis '''U'''ne '''N'''ouvelle '''P'''lanète. *'''''M'''e '''V'''oici, '''T'''onton '''M'''arcel, '''J'''e '''S'''uis '''U'''n '''N'''ageur '''P'''rofessionnel. *'''''M'''e '''V'''oici, '''T'''out '''M'''ouillé, '''J'''e '''S'''uis '''U'''n '''N'''ageur '''P'''ressé. *'''''M'''e '''V'''oici, '''T'''out '''M'''ouillé, '''J''''ai '''S'''uivi '''U'''n '''N'''uage '''P'''luvieux. *'''''M'''ais '''V'''iendras-'''T'''u '''M'''anger, '''J'''ulie, '''S'''ur '''U'''ne '''N'''appe '''P'''ropre. *'''''M'''on '''V'''ieux '''T'''héâtre '''M'''e '''J'''oue '''S'''ouvent '''U'''ne '''N'''ouvelle '''P'''ièce. *'''''M'''on '''V'''ieux, '''T'''u '''M''''as '''J'''eté '''S'''ur '''U'''ne '''N'''ouvelle '''P'''lanète. *'''''S'''ors (pour Soleil) -'''M'''oi '''V'''ite '''T'''a '''M'''armite '''J'''aune '''S'''ur '''U'''ne '''N'''appe '''P'''ropre.'' *'''''M'''onsieur '''V'''ous '''T'''irez '''M'''al '''J'''e '''S'''uis '''U'''n '''N'''ovice '''P'''itoyable. * '''''Mé'''lanie, '''V'''ous '''T'''ombez '''Ma'''l, '''J'''e '''S'''uis '''U'''n '''N'''avet '''P'''ourri.'' *'''''M'''on '''V'''aisseau '''T'''e '''M'''ènera '''J'''eudi '''S'''ur '''U'''ne '''N'''ouvelle '''P'''lanète. *'''''M'''a '''V'''ieille '''T'''ante '''M'''arge '''J'''oue '''S'''ur '''U'''n '''N'''ouveau '''P'''iano. *'''''M'''aman '''V'''ole '''T'''ous '''M'''es '''J'''ouets, '''S'''auf '''U'''n '''N'''ounours '''P'''ourri !'' *'''''M'''arin '''V'''aleureux, '''T'''u '''M'''ourras '''J'''eune '''S'''ur '''U'''n '''N'''avire '''P'''erdu !'' *'''''M'''on '''V'''élo '''T'''ourne '''M'''al '''J'''e '''S'''uis '''U'''n '''N'''ouveau '''P'''iéton. *'''''M'''erci '''V'''ous '''T'''ous '''M'''aintenant '''J'''e '''S'''ais '''U'''nir '''N'''euf '''P'''lanètes. *'''''M'''élanie '''V'''ous '''T'''ombez '''M'''al '''J'''e '''S'''uis '''U'''n '''N'''avet '''P'''ourri. *'''''M'''es '''V'''ieilles '''T'''antes '''M'''angeaient '''J'''adis '''S'''ur '''U'''ne '''N'''appe '''P'''ercée.'' *'''''M'''ets '''V'''ite '''T'''on '''M'''aillot '''J'''e '''S'''uis '''U'''n '''N'''udiste '''P'''oilu. * '''''M'''on '''V'''ieux '''T'''acot '''M'''´a '''J'''eté '''S'''ur '''U'''n '''N'''oble '''P'''''assant. *'''''M'''ange '''V'''ite '''T'''on '''M'''ars '''J''' 'en '''S'''ors '''U'''n '''N'''ouveau '''P'''aquet. *'''''MÈR'''E, '''V'''iens '''Ter'''miner '''M'''a '''JUP'''e, '''SA''' cout'''UR'''e '''NE''' tient '''PLU'''s.'' * '''''M'''aman, '''V'''oudrais-'''T'''u '''M''''emmener '''J'''ouer '''S'''ur '''U'''ne '''N'''ouvelle '''P'''lanète ?'' Suite à un concours qui s’est déroulé au Québec, la formule suivante a été retenue : *'''''M'''angez '''V'''os '''T'''artes, '''M'''ais '''J'''uste '''S'''ur '''U'''ne '''N'''appe !'' Il existe aussi cette formule (la plus ancienne mnémonique connue en astronomie) qui se retient facilement, grâce à ses trois mots de trois syllabes : *''Merveter, Marjusa, Uneplu'' ('''Mer'''cure, '''Vé'''nus, '''Ter'''re, '''Mar'''s, '''Ju'''piter, '''Sa'''turne, '''U'''ranus, '''Nep'''tune, '''Plu'''ton) Une variante<ref>Formule tirée de l’un des tomes du [https://fr.m.wikipedia.org/wiki/Manuel_des_Castors_Juniors ''Manuel des Castors Juniors'']</ref> de celle-ci : * ''Mervé'', ''Termaju'', ''Saturneplu'' ('''Mer'''cure, '''Vé'''nus, '''Ter'''re, '''Ma'''rs, '''Ju'''piter, '''Sat'''urne, '''Ur'''anus, '''Ne'''ptune, '''Plu'''ton) Qui existe aussi sous cette forme : * ''Mervé, Termaju, Satur n'est plus'' Et celle-ci qui inclut le Soleil : *'''''S'''alut ! '''Me''' '''v'''ois-'''t'''u ? '''M'''oi '''j'''e '''s'''uis '''u'''ne '''n'''ouvelle '''pl'''anète !'' '''Planète ayant un système d'anneaux''' * '''J'''e '''S'''uis '''U'''ne '''N'''ouille (Jupiter, Saturne, Uranus, Neptune) ==== Ordre des quatre lunes principales de Jupiter ==== '''I'''l '''e'''st '''g'''rand, '''C'''harles ! * [[w:Io|Io]], [[w:Europe|Europe]], [[w:Ganymède_(lune)|Ganymède]], [[w:Callisto_(lune)|Callisto]] ==== Croissant de Lune ==== Le '''p'''remier croissant et le '''d'''ernier croissant peuvent être reconnus en les assimilant aux sens du p et du d. En effet, en « ajoutant » au croissant de lune un bâton, on obtient un p ou un d selon le croissant. Cette méthode marche uniquement dans l'[[w:Hémisphère (géographie)|hémisphère]] [[w:nord|nord]], dans l’hémisphère sud il faudra considérer que la Lune ment. Une méthode plus simpliste consistait autrefois à lire le croissant de lune directement. Quand il formait un '''C''' la lune incitait à penser qu'elle était '''C'''roissante . Or dans ce cas là elle est décroissante. Et quand elle formait un '''D''' (en supposant l’ajout de la barre droite nécessaire) elle incitait à penser qu’elle était '''D'''écroissante. Or dans ce cas là elle est croissante. Il en est venu l’expression populaire : ''Il est menteur comme la lune''. Cependant, dans ce cas la Lune ne ment que dans l'hémisphère Nord : C correspond bien à la Lune croissante et D à la Lune décroissante. Ces méthodes ne sont pas valables entre les tropiques, où le sens de ''lecture'' varie selon les saisons. ==== [[w:type spectral|Types spectraux]] [[w:étoile|stellaires]] ==== Les différents [[w:type spectral|types spectraux]], du plus chaud au plus froid, sont : O, B, A, F, G, K, M. '''''O'''h, '''b'''e '''a''' '''f'''ine '''g'''irl/'''g'''uy, '''k'''iss '''m'''e !'' '''''O'''verseas '''b'''roadcast: '''a''' '''f'''lash! [[w:Godzilla|'''G'''odzilla]] '''k'''ills [[w:Mothra|'''M'''othra]] !'' === Physique === ==== Électromagnétique ==== Énergie électrique stockée dans un condensateur&nbsp;: <math>E= (1/2) C U^2</math> "l'''e''' '''demi''' '''cu'''l '''carré'''" ==== Les sept unités fondamentales==== Pour: ''seconde, ampère, candela, kilogramme, mètre, kelvin, mole&nbsp;'': <br> Sac km km <br> Je <u>'''s'''ais q</u>u<u>'''a'''n</u>d <u>'''c'''a</u>c<u>'''k'''i</u> <u>'''m'''et</u> <u>'''k'''el</u> <u>'''m'''o</u>t ! : (je) ''Sec-Am-Ca-Ki-Mè-Kel-Mo'' <br> <u>Ce con</u> d'<U>Ampère</U>, <u>qu'en d</u>it <u>qui l'au</u>ra!, <u>mettr</u>a <u>quel vin</u> au <u>môl</u>e ?: ''Sec<s>onde</s>'' d' ''ampère'', ''cand<s>ela</s>'' ''kilo<s>gramme</s>''ra, ''mètr<s>e</s>''a ''kelvin'' au ''moles'' "Secondes molles, quand des lacs-îlots [[wikt:grammer|grammant]] [[wikt:pairer|pairent]], Maître Kelvin !" (qui se prononce comme : "seconde, mole, candela, kilogramme, ampère, mètre, kelvin") * ==== Ordre des couleurs de [[w:Résistance (composant)#Repérage et valeurs normalisées|résistance électrique]] ==== ('''N'''oir, '''M'''arron, '''R'''ouge, '''O'''range, '''J'''aune, '''V'''ert, '''B'''leu, '''Vio'''let, '''G'''ris, '''B'''lanc) '''''N'''e '''M'''ange '''R'''ien '''O'''u '''J'''e '''V'''ais '''B'''leuir '''V'''iolemment (ton) '''G'''ros '''B'''laze.'' '''''N'''e '''M'''angez '''R'''ien '''O'''u '''J'''e '''V'''ous '''B'''rule '''V'''otre '''G'''rosse '''B'''arbe.'' '''''N'''e '''M'''angez '''R'''ien '''O'''u '''J'''eûnez, '''V'''oilà '''B'''ien '''V'''otre '''G'''rande '''B'''êtise.'' '''''N'''e '''M'''angez '''R'''ien '''O'''u '''J'''e '''V'''ous '''B'''rise '''V'''otre '''G'''rosse '''B'''outeille.'' '''N'''e '''M'''angez '''R'''ien '''O'''u '''J'''e '''V'''ous '''B'''ats '''V'''iolemment '''G'''ros '''B'''êta '''''N'''adine '''M'''e '''R'''épondit''' O'''ui, '''J'''e '''V'''eux '''B'''ien '''V'''otre '''G'''rosse '''B'''iroute'' '''''N'''oir, '''M'''arron, les couleurs de l'arc en ciel (sauf l'indigo), '''G'''ris, '''B'''lanc.'' Version québécoise utilisant la lettre '''B''' pour Brun au lieu de Marron : '''''N'''otre '''B'''ar '''R'''estera '''O'''uvert '''J'''eudi et '''V'''endredi. '''B'''ière et '''V'''in '''G'''ratuit, '''B'''ienvenue.'' '''''N'''otre '''B'''ar '''R'''estera '''O'''uvert '''J'''eudi et '''V'''endredi. '''B'''ien'''V'''enue '''G'''ros '''B'''uveur.'' ==== Ordre des couleurs du [[w:Couleur#Le spectre lumineux|spectre visible]] ==== Les sept couleurs du spectre visible ou de l'arc-en-ciel (dans l'ordre des fréquences croissantes : '''R'''ouge - '''O'''range - '''J'''aune - '''V'''ert - '''B'''leu - '''I'''ndigo - '''V'''iolet) peuvent se retenir grâce à la phrase suivante : La '''ROU'''sse '''OR'''ienta le '''J'''uge '''VER'''s le '''BL'''azer de l''''INDI'''enne '''VIOL'''ée. Dans l'ordre inverse (soit de la plus petite à la la plus grande longueur d'onde) elles peuvent se retenir grâce au mot '''VIBUJOR''', en remplaçant le '''U''' par un '''V''' ('''vert''' comme '''Hu'''lk). '''V'''iolet - '''I'''ndigo - '''B'''leu - '''V'''ert - '''J'''aune - '''O'''range - '''R'''ouge Remarque : en se figurant le drapeau français '''bleu'''-'''blanc'''-'''rouge''', on peut retrouver l’ordre des longueurs d’onde, en assimilant le bleu à l’ultraviolet, le blanc au visible, et le rouge à l’infra-rouge : '''ultraviolet'''-'''visible'''-'''infra-rouge'''. ==== Longueur d'onde des couleurs ==== Le mot rouge est plus long que le mot bleu (5 vs 4), sa longueur d'onde est plus longue également. ==== Couleurs en peinture et rayonnements lumineux ==== Les peintres utilisent les trois couleurs fondamentales '''Cyan Magenta et Jaune''', chacune absorbant une seule des trois couleurs fondamentales de la lumière (Rouge Vert et Bleu). Notre œil ne reconnaît la couleur que par la lumière identifiée par chacune des 3 familles de cônes de l'œil respectivement sensibles aux rayonnements '''Rouge Vert et Bleu'''. Cette phrase permet aux peintres et aux physiciens d'identifier un équivalent des deux couleurs de rayonnement lumineux identifiées par les cônes de l'œil pour chacune des couleurs fondamentales de la peinture. '''C'''ette '''B'''onne '''V'''ieille<br /> '''M'''ijote des '''R'''aviolis "'''B'''uitoni"<br /> '''<nowiki>J'</nowiki>'''en '''R'''é'''V'''ais La couleur '''C'''yan de la peinture correspond ainsi à la réception des rayonnements lumineux '''B'''leu et '''V'''ert. Le '''M'''agenta, pour sa part correspond aux rayonnements lumineux '''R'''ouge et '''B'''leu. Quant à la couleur '''J'''aune, elle renvoie vers l'œil les rayonnements lumineux '''R'''ouge et '''V'''ert. ==== Constantes ==== * vitesse de la lumière<ref name="villemin.gerard" /> : :{| style="text-align: center;" |Ah,||messagère||admirable,||lumière||éclatante,||je||sais||votre||célérité|| |- |La||constante||lumineuse||restera||désormais||là||dans||votre||cervelle|| |- |2||9||9||7||9||2||4||5||8||m/s |} * définition formelle d'une seconde (périodes de la radiation correspondant à une transition entre les deux sous-niveaux hyperfins du césium 133) : :{| style="text-align: center;" |« Pharaonne,||j'||affirmais||là,||honore||mal||l'||aimable||seconde||0 ! » |- |9||1||9||2||6||3||1||7||7||0 |} === Chimie === '''Priorité des groupes caractéristiques en nomenclature''' Pour nommer une molécule composée de plusieurs groupes caractéristiques, on utilise l'ordre suivant : Acide carboxylique - anhydride d'acide - ester - halogénure d'acyle - amide - nitrile - aldéhyde - cétone - alcool - amine - alcyne - alcène - éther-oxyde - dérivé halogéné - alcane Pour se rappeler de l'ordre : '''Ac'''e '''an'''nule '''Ester''' ! '''Halo ami'''? '''Ni'''e '''al'''ors '''Cé'''cile '''alcool'''isée, '''Am'''élie '''ascene''' à N'''eoxi''' et '''De'''lp'''h'''ine un '''alcane''' ==== Radicaux alkyles ==== Pour se rappeler l’ordre des 3 premiers groupement alcanes : * Il ai'''mait''' '''êt'''re '''pro'''pre. (Oralement, "Il ai'''Mét Éth Prop''' ") Pour se rappeler l’ordre des 4 premiers groupement alcanes : * ('''M'''éthane, '''É'''thane, '''P'''ropane, '''B'''utane) * '''M'''aman '''Et''' '''P'''apa '''B'''ébé * '''M'''aman '''Et''' '''P'''apa '''But'''inent. * '''M'''alin qui '''É'''tudie '''P'''our le '''B'''ac. * '''M'''ieux '''É'''tudier '''P'''our le '''B'''ac. * '''M'''on '''É'''cole '''P'''eut '''B'''rûler. * '''M'''on '''É'''lève '''P'''isse '''B'''ien * '''M'''organe '''E'''st '''P'''as '''B'''elle. * '''Me'''s '''é'''lèves '''p'''arlent '''b'''eaucoup. * '''M'''ets '''t'''es '''Prop'''res '''But'''s ! (Oralement, "Mét Éth Prop But") Pour se rappeler l’ordre des 5 premiers groupement alcanes : * ('''M'''éthane, '''É'''thane, '''P'''ropane, '''B'''utane, '''P'''entane) * '''M'''aman '''E'''st '''P'''artie '''B'''ébé '''P'''leure * '''M'''amie '''E'''st '''P'''artie '''B'''oire une '''P'''inte Pour se rappeler l’ordre des 6 premiers groupement alcanes : * ('''M'''éthane, '''É'''thane, '''P'''ropane, '''B'''utane, '''P'''entane, '''H'''exane) * '''M'''aurice '''E'''st '''P'''as '''B'''eau '''P'''our '''H'''élène. * '''M'''amie '''E'''t '''P'''api '''B'''atifolent '''P'''endant l''''H'''iver. * '''M'''aman '''E'''t '''P'''apa '''B'''oivent '''P'''endant '''H'''alloween * '''M'''amie '''E'''st '''P'''artie '''B'''oire une '''P'''inte de '''H'''eineken * '''M'''et '''E'''th '''P'''rop '''B'''ut '''P'''ent '''H'''ex Pour les plus vulgaires : * '''M'''audite '''É'''paisse ! '''P'''ourquoi '''B'''aiser '''P'''our l' '''H'''iver ! ==== [[w:Tableau périodique des éléments|Tableau périodique des éléments]] ==== Il est à noter que la plupart des moyens mnémotechniques concernant les éléments ont été créés par des [[w:étudiant|étudiant]]s, d’où le [[w:vocabulaire|vocabulaire]] parfois amusant des maximes. ===== [[w:Éléments de la période 2|Période 2]] ===== '''Pour : Li'''thium, '''Bé'''ryllium, '''B'''ore, '''C'''arbone, '''N'''itrogène (Azote), '''O'''xygène, '''F'''luor, '''Né'''on.'' * «La '''Li''''''Bé'''llule '''B'''leue, d’une '''C'''aresse, '''N'''oit dans l’'''O'''nde la '''F'''leur de '''Né'''nuphare. » * « '''Li'''thus et '''Be'''rénice '''B'''oivent, '''C'''haque '''N'''uit, '''O''' '''F'''rais de '''Né'''ron » * « '''Li'''verpool, '''Be'''rceau des '''B'''eatles, '''C'''onnait '''N'''aturellement ces '''O'''librius '''F'''ous et '''Né'''vrosés » * « '''Li'''bérez '''Be'''n '''B'''arkans, '''C'''élèbre '''N'''arrateur, '''O'''u '''F'''usillez '''Né'''ron » * « '''Li''' '''Be''' le '''B'''on '''C'''anard du '''N'''ord '''O'''uest de la '''F'''rance '''Ne'''ogauchiste » * « '''Li'''li '''Be'''sa '''B'''ien '''C'''ouchée '''N'''ue '''O''' '''F'''lanc de '''Né'''ron » * « '''Li'''li '''Be'''se '''B'''ien '''C'''hez '''N'''otre '''O'''ncle '''F'''umeur de '''Ne'''squik » * « '''Li'''li '''Be'''se '''B'''ien '''C'''onfortablement '''N'''otre '''O'''ncle '''F'''rançois '''Ne'''stor » * « '''Li'''mace '''Be'''te '''B'''ouffa '''C'''inq '''N'''ouveaux '''O'''ignons '''F'''raîchement '''Né'''s » * « '''Li'''li '''Be'''rça '''B'''ébé '''C'''hez '''N'''otre '''O'''ncle '''F'''ernand '''N'''estor * « '''Li'''li '''B'''ecta '''B'''ien '''C'''hez '''N'''otre '''O'''ncle '''F'''erdinand '''N'''estor » * « '''Li'''li '''Bé'''cha '''B'''ien '''C'''hez '''N'''otre '''O'''ncle '''F'''rançois-(ou '''F'''erdinand-)'''Ne'''stor »<br /> * « '''Li'''li '''Bé'''se '''B'''ien '''C'''hez '''N'''otre '''O'''ncle '''F'''rançois-(ou '''F'''erdinand-)'''Ne'''stor »<br /> * « '''LiBe'''rté '''B'''afouée '''C'''ontre '''N'''otre '''O'''rganisation '''F'''édérale '''Né'''ogaulliste (ou '''Né'''otrotskiste) » * « le '''Li''' t de '''BE''' '''B''' é a '''C'''assé le '''N'''ez de l' '''O'''ncle '''F'''urieux '''Né'''on » * « '''Li'''vrez '''Bê'''tement '''B'''ataille '''C'''ar '''N'''ous, '''O'''fficiers '''F'''rançais, '''Né'''gocions » * « '''L'i'''magination '''Be'''lliqueuse '''B'''aissa '''C'''ar '''N'''otre '''O'''rdre '''F'''ut '''Ne'''t » * « '''Li'''re '''Be'''aucoup '''B'''alzac '''C'''ar '''N'''otre '''O'''rthographe '''F'''ait '''Né'''gligé » * « '''Li'''ste de '''Be'''lles '''B'''outeilles de '''C'''ognac '''N'''ous '''O'''nt '''F'''outus '''Ne'''rveux » * « '''Li'''li '''Be'''cote '''B'''ien '''C'''omme '''Ni'''cole '''O''' '''F'''ond '''N'e'''st ce pas » * « '''Li'''bérez '''Be'''rnard '''B'''ossu '''C'''ontre '''N'''ouvel '''O'''tage '''F'''éminin. Signé '''Ne'''on » * « '''LiBe'''rté de '''B'''oire '''C'''ar '''N'''ous '''O'''n '''F'''oire '''N'''os '''e'''xams » * « '''Li'''bérez '''Be'''n '''B'''arka '''C'''ar '''N'''ous, '''O'''fficiers '''F'''rançais, '''Né'''gocions » * « '''Li'''li et '''Be'''rnard '''B'''aisent '''C'''omme '''N'''ous '''O'''n '''F'''ait '''Ne'''spa » ===== [[w:Éléments de la période 3|Période 3]] ===== ''Pour : '''S'''odium, '''M'''a'''g'''nésium, '''Al'''uminium, '''Si'''licium, '''P'''hosphore, '''S'''oufre, '''C'''h'''l'''ore, '''Ar'''gon.'' * « '''S'''uzanne '''M'''an'''g'''ea '''Al'''lègrement '''Si'''x '''P'''russiens '''S'''ans '''Cl'''aquer '''A'''p'''r'''ès. » * « '''S'''uzanne '''M'''an'''g'''ea '''Al'''lègrement '''Si'''x '''P'''oulets '''S'''ans '''Cl'''aquer des '''Ar'''ticulations. » * « '''S'''uzanne '''M'''an'''g'''ea '''Al'''lègrement '''Si'''x '''P'''oulets (ou '''P'''erdrix) '''S'''ans '''Cl'''aquer d' '''Ar'''gent. » Le sodium est représenté par '''Na'''. Alors Napoléon remplace Suzanne pour retrouver le symbole : * « '''Na'''poléon '''M'''an'''g'''ea '''Al'''lègrement '''Si'''x '''P'''oulets '''S'''ans '''Cl'''aquer d''''Ar'''gent » — ou « sans claquer '''A'''p'''r'''ès », « d''''Ar'''gon », « d''''Ar'''tère », « (d')'''Ar'''tiche » ou « sans claquer les '''Ar'''ticulations » pour éviter la confusion avec l'élément argent, noté '''Ag'''. * « '''Na'''poléon '''M'''an'''g'''ea '''Al'''lègrement '''Si'''x '''P'''russiens '''S'''ans '''Cl'''ore l’'''Ar'''mistice. » * « '''Na'''guère '''M'''onsei'''g'''neur '''Al'''louche '''Si''' '''P'''ervers '''S'''uça '''Cl'''aire '''Ar'''demment. » * « '''Na'''poléon '''M'''an'''g'''eait '''Al'''lègrement '''Si'''x '''P'''oulets '''S'''ans '''Cl'''amser '''A'''p'''r'''ès. » * « '''Na'''poléon '''M'''a'''g'''nera '''À''' '''l'''<nowiki/>'est '''Si''' '''P'''ossible '''S'''a '''C'''o'''l'''onne '''A'''rmée. » *« '''Na'''billa '''M'''an'''g'''e (h)'''Al'''lal '''Si''' '''P'''atrick '''S'''ébastien '''Cl'''ash '''Ar'''thur. » ===== [[w:Éléments de la période 4|Période 4]] ===== ''Pour : '''K'''allium (Potassium), '''Ca'''lcium, '''Sc'''andium, '''Ti'''tane, '''V'''anadium, '''C'''h'''r'''ome, '''M'''a'''n'''ganèse, '''Fe'''r, '''Co'''balt, '''Ni'''ckel, '''Cu'''ivre, '''Z'''i'''n'''c, '''Ga'''llium, '''Ge'''rmanium, '''A'''r'''s'''enic, '''Sé'''lénium, '''Br'''ome, '''Kr'''ypton.'' * « '''K'''arl '''Ca'''pitaine '''Sc'''andinave '''Ti'''ra sa '''V'''erge '''Cr'''asseuse et '''M'''i'''n'''uscule, '''Fé'''conda le '''Co'''n de '''Ni'''cole, et le '''Cu'''l de ses '''Z'''e'''n'''nemies, '''Ga'''rdant '''Ge'''néreusement '''As'''sez de '''Se'''mence pour ce '''Br'''ave '''K'''h'''r'''ouchtchev. » * « '''K'''évin '''Ca'''pture un '''Sc'''arabée '''Ti'''mide dans le '''V'''agin '''Cr'''éatif de '''M'''o'''n'''ique, '''Fé''' (fait) '''Co'''mme '''Ni'''cole dans le '''Cu'''l en '''Zinc''' de '''Ga'''spard de '''Ge'''rmanie, puis '''As'''pire '''Sé'''bastien dans la '''Br'''aguette du '''Kr'''aken » * « '''K'''hrouchtchev '''Ca'''ressa '''Sc'''iemment '''Ti'''to. '''V'''orochev '''Cr'''ia '''M'''ag'''n'''anime : "'''Fé''' pas le '''Co'''n '''Ni'''kita, ton '''Cu'''l en '''Z'''i'''n'''c '''Ga'''lvanisé te '''Gè'''ne '''AsSe'''z pour '''Br'''anler des '''Kr'''evettes. » * « '''K'''épler '''Ca'''lculait des '''Sc'''alaires '''Ti'''tanesques, '''V'''oyant '''Cr'''o-'''M'''ag'''n'''on '''Fé'''sant le '''Co'''n '''Ni'''ché sur le '''Cu'''l d'un '''Z'''ébulo'''n''', '''Ga'''gnant '''Gé'''néralement '''AsSe'''z de '''B'''iè'''r'''es '''Kr'''onenbourg. » * « '''K''' '''Ca''' (cacas) '''Sc'''iés de '''Ti'''ti '''V'''olant et '''Cr'''os '''M'''i'''n'''et qui '''F'''ont ('''Fe''') des '''Co'''nneries ont '''Ni'''qué le '''Cu'''l à '''Z'''a'''n'''zibar d'un '''Ga'''rs '''Gé'''nial '''As'''sis '''Se'''rrant une '''B'''onne ('''Br''') '''Kr'''o. » * « '''K'''hrouchtchev '''Ca'''ressa '''Sc'''andaleusement '''Ti'''tov. '''Van'''ia '''Cr'''ia '''M'''ag'''n'''animement "'''F'''ais pas le '''Co'''n Nikita, la Cuisine en '''Z'''i'''n'''c de la '''Ga'''re de '''Ge'''nève '''As''' Ses '''Br'''iques '''Cr'''euses » (ou '''K'''hrouchtchev '''Ca'''ssa le '''SC'''ooter à '''TI'''tov) * « '''K'''hrouchtchev '''Ca'''ressa '''Sc'''andaleusement la '''Ti'''gnasse de '''V'''anadia, '''Cr'''oyant '''M'''a'''n'''ifestement '''Fe'''re '''Co'''cu '''Ni'''colaiev, le '''Cu'''ré de '''Z'''a'''n'''zibar '''Ga'''gna '''Ge'''nève '''As'''sez '''Se'''crètement avec Son '''Br'''éviaire '''Kr'''ipté. » * « '''K'''arl '''Ca'''valier '''Sc'''andinave '''Ti'''ra '''V'''engeance '''C'''r'''u'''elle '''M'''a'''n'''iant le '''Fe'''r '''Co'''ntre le '''Ni'''kel. Le '''Cu'''l de '''Z'''e'''n'''obie '''Ga'''rnit de '''Ge'''ranium '''As'''pire la '''Se'''ve '''Br'''ûlante du '''Kr'''atère (cratère/Krypton). » * « '''K'''onrad '''Ca'''pitaine '''Sc'''andinave '''Ti'''ra sa '''V'''erge '''Cr'''asseuse et '''M'''i'''n'''uscule, '''Fe'''rmant le '''Co'''n de '''Ni'''cole, le '''Cu'''l de '''Zn'''obie, et '''Ga'''rdant '''Ge'''néreusement l''''As'''permique '''Se'''mence du '''Br'''ave '''K'''e'''r'''mit. » * « '''K'''a'''Ca''' '''Sc'''quatte avec '''Ti'''ti la '''V'''oiture de '''Cr'''os '''M'''i'''n'''et. '''Fe'''rnand '''Co'''nduit sa mi'''Ni''' '''COOPER''' en '''Z'''i'''g'''za'''Ga'''nt, '''Ge'''néralement '''As'''sis en se '''Se'''rvant une '''B'''ière '''Kr'''onembourg. » * « '''K'''hrouchtchev '''Ca'''ressa '''Sc'''rupuleusement le '''Ti'''tanesque et '''V'''elue '''Cr'''ane du '''M'''o'''n'''de avec '''Fe'''rmeté. '''Co'''ntre l'ennemie, '''Ni'''kita '''C'''a'''u'''sa la '''Z'''iza'''n'''ie, il en'''Ga-Ge'''a l''''As'''sault '''S'''talinien. '''Br'''avo '''K'''h'''r'''ouchtchev. » * « '''K'''af'''Ca''' (Kafka) '''Sc'''ruta, '''Ti'''mide, '''V'''era '''Cr'''uz '''M'''o'''n'''trant ses '''Fe'''sses, '''Co'''mme la '''Ni'''mphe (nymphe) '''Cu'''pide '''Z'''é'''n'''a. '''Ga'''llien et '''Ge'''rard, '''As'''ssis, '''Se''' '''Br'''assaient de la '''Kr'''onenbourg. » * « '''K'''orrigan '''Ca'''pitaine '''Sc'''andinave '''Ti'''rant sa '''V'''erge '''Cr'''asseuse et '''M'''i'''n'''uscule ('''Mn''') '''Fe'''rma le '''Co'''n de '''Ni'''cole et le '''Cu'''l de '''Z'''é'''n'''obie ('''Zn''') '''Ga'''rdant '''Gé'''néreusement l’'''As'''permatique '''Se'''mence d’un '''Br'''un '''Kr'''omatique (chromatique).» *« '''K'''évin '''Ca'''sse '''Sc'''iemment sa '''Ti'''relire et '''V'''ient '''Cr'''ier : "'''M'''ama'''n''', fait ('''Fe''') '''Co'''mme '''Ni'''cole, '''Cu'''isine !". '''Z'''hi'''n'''g lui dit : "dé'''GaGe''', '''AsSe'''z '''Br'''aillé '''Kr'''étin". * « '''K'''arine '''Ca'''lcula que '''Sc'''ientifiquement '''Ti'''tillé, '''V'''incent '''Cr'''ame '''M'''o'''n''' '''F'''outr'''e''' '''Co'''mme '''Ni'''colas '''Cu'''pide '''Z'''i'''n'''zin '''Ga'''lleux '''Ge'''sticulant '''As'''ymétriquement et '''Se''' '''Br'''ulant à la '''Kr'''yptonite. » * « '''K'''ptain '''Ca'''ca '''Sc'''andinave '''Ti'''re sa '''V'''erge '''Cr'''asseuse et '''M'''i'''n'''uscule (Mn) des '''Fe'''sses de '''Co'''rine '''Ni'''çoise, '''Cu'''ltivée et '''Z'''e'''n''' (Zn), '''Ga'''lamment '''Ge'''néreuse, '''As'''sez '''Se'''xy et '''Br'''anlant '''K'''a'''r'''im (Kr). » * « '''K'''évin '''Ca'''tapulta '''Sc'''iemment '''Ti'''bère le '''V'''erreux, le '''Cr'''étin, le '''M'''écha'''n'''t, '''Fe'''roce, '''Co'''rrompu. Ha'''Ni'''bal, '''Cu'''i'''Z'''a'''n'''t, '''Ga'''ve '''Ge'''ntillement d''''As'''pirine '''Se'''c '''Br'''utus le '''Kr'''asseux. » * « '''K'''arine '''Ca'''ressa '''S'''e'''c''' '''Ti'''mothée '''V'''ers sa '''Cr'''oupe '''M'''ais '''n'''e '''Fé'''(fait) '''Co'''uille '''Ni''' '''Cu'''l. '''Z'''ho'''n'''g '''Ga'''gna '''Ge'''ntiment '''A s'''e '''Br'''anler '''Kr'''asseusement. » * « '''K'''oalas de '''Ca'''nberra, '''S'''’é'''c'''ria-'''T'''-'''i'''l, je '''V'''eux '''Cr'''oire '''M'''o'''n''' '''F'''r'''è'''re '''Co'''mplètement ! Ils '''Ni'''chent, '''C'''op'''u'''lent en '''Z'''o'''n'''ages '''Ga'''lamment '''Gé'''rés, '''As'''sistés '''Se'''ulement de '''Br'''ouillons '''K(r)'''yptés. (variante : de '''Br'''aves '''K'''angou'''r'''ous)» * '''K'''arl '''Ca'''usa '''Sc'''andale. '''Ti'''ens, '''V'''oilà '''C'''a'''r'''rément '''M'''o'''n'''sieur '''FeCoNi''' '''Cu'''ltivant '''Z'''o'''n''' '''Ga'''rdin. '''Ge'''rmaine, '''As'''sise, '''Se''' '''Br'''osse le '''Kr'''âne. * '''K'''hrouchtchev, '''Ca'''lant son '''Sc'''rotum '''Ti'''tanesque '''V'''ers la '''Cr'''oupe '''M'''ag'''n'''ifique des '''Fe'''sses de '''Co'''ssiga, '''Ni'''qua ses '''Cu'''isses dé'''Z'''i'''n'''guées: '''Ga'''lipettes '''Gê'''nantes, '''As'''sez '''Se'''xy pour '''Br'''anler '''Kr'''ouchtchev. * '''K'''rouchtchev, '''Ca'''libre '''Sc'''rotal '''Ti'''tanesque, '''V'''a '''Cr'''euser '''M'''o'''n''' '''Fe'''ssier '''Co'''quin. '''Ni'''quant '''Cu'''pidement '''Z'''i'''n'''oviev, '''Ga'''garine '''Gè'''re '''As'''sez '''Se'''s '''Br'''asseries '''Kr'''onenbourg. * '''K'''rouchtchev '''Ca'''resse '''Sc'''rotalement '''Ti'''to. «'''V'''oilà», '''Cr'''ie '''M'''arc (Mn) '''Fe'''uillée, «'''Co'''mment '''Ni'''kita '''Cu'''lbute!» «'''Z'''i'''n'''zin!» '''Ga'''léjea '''Gé'''rard, «'''As'''sez '''Se'''xy! '''Br'''avo, '''Kr'''ouchtchev!» * '''K'''rouchtchev '''Ca'''usa '''Sc'''andale. '''Ti'''to, '''V'''erge '''Cr'''amoisie, '''M''''e'''nFe'''rre '''Co'''chonnement les '''Ni'''bards et les '''Cu'''isses. '''Z'''é'''n'''obie '''Ga'''ve '''Gé'''rard, '''As'''pergeant '''Se'''s '''Br'''asseries '''Kr'''onenbourg. ===== [[w:Éléments de la période 5|Période 5]] ===== ''Pour : '''R'''u'''b'''idium, '''S'''t'''r'''ontium, '''Y'''ttrium, '''Z'''i'''r'''conium, '''N'''io'''b'''ium, '''Mo'''lybdène, '''T'''e'''c'''hnétium, '''Ru'''thénium, '''Rh'''odium, '''P'''alla'''d'''ium, '''A'''r'''g'''ent, '''C'''a'''d'''mium, '''In'''dium, '''S'''ta'''n'''num (Étain), '''S'''ti'''b'''ium (Antimoine), '''Te'''llure, '''I'''ode, '''Xé'''non.'' * « '''R'''o'''b'''in '''S'''u'''r''' '''Y'''vette a le '''Z'''èb'''r'''e '''N'''o'''b'''le de '''Mo'''nsieur '''T'''u'''c''' '''Ru'''. '''R'''o'''h'''an '''P'''ru'''d'''emment '''Ag'''é '''C'''é'''d'''a '''In'''évitablement '''S'''a'''n'''s '''S'''u'''b'''ir '''Te'''s '''I'''dées '''Xé'''nophobes. » (variante : '''R'''o'''b'''in '''S'''o'''r'''t '''Y'''von, le '''Z'''èb'''r'''e '''N'''o'''b'''le, dans '''Mo'''n '''T'''a'''c'''ot '''R'''o'''u'''illé) * «'''R'''o'''b'''ert '''S'''enio'''r''' est un '''Y'''éti du '''Z'''aï'''r'''e ou un '''N'''o'''b'''lio de '''Mo'''dène. Le '''T'''e'''c'''hnicien '''Ru'''dement bourré au '''Rh'''um '''P'''é'''d'''ale '''Ag'''ilement des '''C'''ou'''d'''es, il '''I'''nsulte '''S'''ai'''n'''t '''S'''e'''b'''astien, un '''Te'''rrien '''I'''diot et '''Xé'''nophobe. » * « '''R'''u'''b'''y, '''S'''o'''r'''te de '''Y'''éti '''Z'''aï'''r'''ois qu’un '''N'''o'''b'''liau de '''Mo'''dène, '''T'''e'''c'''hniquement '''Ru'''iné, '''R'''ac'''h'''ète au '''P'''ala'''d'''in avec l’'''A'''r'''g'''ent des '''C'''a'''d'''eaux '''In'''digènes '''S'''a'''n'''s '''S'''u'''b'''ir '''Te'''s '''I'''res '''Xé'''nophobes. » * « Le '''R'''a'''b'''bin '''S'''o'''r'''t son '''Y'''acht, le '''Z'''èb'''r'''e, pendant que '''N'''a'''b'''il, le '''Mo'''ldave '''T'''ur'''c''' '''Ru'''dement bourré au '''Rh'''um '''P'''é'''d'''ale '''Ag'''ilement des '''C'''ou'''d'''es, '''In'''diquant à '''S'''o'''n''' ami '''S'''é'''b'''astien la '''Te'''rre '''I'''mbibée de '''Xé'''rès. » * « '''R'''o'''b'''in '''S'''′'''r'''approche du '''Y'''éti sur un '''Z'''èb'''r'''e, '''No'''nobstant le '''Mo'''rse '''Tc'''hèque en '''Ru'''t et le '''Rh'''inocéros '''P'''é'''d'''é '''Ag'''ressif et '''C'''an'''d'''ide ; '''In'''capable de '''Sn'''iffer du '''S'''a'''b'''le et de la '''Te'''rre en '''I'''mitant '''Xé'''na. » * «'''R'''o'''b'''ert '''S'''enio'''r''', un '''Y'''éti du '''Z'''aï'''r'''e, '''N'''o'''b'''le et '''Mo'''rose, '''T'''ri'''c'''otait '''Ru'''e du '''Rh'''um et, '''P'''en'''d'''ant l′'''Ag'''enouillement du '''C'''i'''d''', '''In'''sulta '''Sn'''obes et '''Sb'''ires '''Te'''rrorisés à l′'''I'''rruption d′un '''Xé'''nophobe. » ===== [[w:Éléments de la période 6|Période 6]] ===== ''Pour : '''C'''é'''s'''ium, '''Ba'''ryum, '''La'''nthane, '''H'''a'''f'''nium, '''Ta'''ntale, '''W'''olfram (Tungstène), '''R'''h'''é'''nium, '''Os'''mium, '''Ir'''idium, '''P'''la'''t'''ine, '''Au'''rum (Or), '''H'''ydrar'''g'''irum (Mercure), '''T'''ha'''l'''lium, '''P'''lom'''b''', '''Bi'''smuth, '''Po'''lonium, '''A'''s'''t'''ate, '''R'''ado'''n'''.'' (La ou Lu selon la classification) * « '''C'''é'''s'''ar '''Ba'''lade '''La''' '''H'''i'''f'''i de '''Ta'''ta dans le '''W'''agon et '''Re'''garde '''Os'''si '''Ir'''ma. '''P'''e'''t'''er '''Au''' '''H'''an'''g'''ar, un '''T'''e'''l''' '''P'''ro'''b'''lème '''Bi'''en '''Po'''sé '''At'''tend '''R'''épo'''n'''se. » (variante : et '''Re'''garde '''Os'''ciller '''Ir'''ma) * « '''C'''é'''s'''ar '''Ba'''isa '''La'''ngoureusement l''''H'''orri'''f'''iante '''Ta'''ntouse dans les '''W'''C ('''Ré'''pugnants), '''Ré'''pétant les '''Os'''cillations '''Ir'''resistibles du '''P'''é'''t'''ard d''''Au'''rélien ; Mercure('''Hg''') lui '''T'''ai'''l'''la dans le '''P'''lom'''b''' une '''Bi'''te '''Po'''ilue pour lui '''A'''s'''t'''iquer les '''R'''ei'''n'''s. » * « '''C'''a'''s'''imir et '''Ba'''stien '''La'''ncent des '''H'''yper'''f'''réquences qui '''Ta'''pent sur un '''W'''agon '''Re'''mpli d''''Os''' '''Ir'''radiés, qui '''P'''é'''t'''a à l''''Au'''be '''H'''y'''g'''iénique, '''T'''é'''l'''éportant un '''P'''lom'''b'''ier '''Bi'''zarre '''Po'''lonais '''At'''taché à la '''R'''ei'''n'''e. » ===== [[w:Éléments de la période 7|Période 7]] ===== Pour : '''Fr'''ancium, '''Ra'''dium, '''Ac'''tinium, '''R'''uther'''f'''ordium, '''D'''u'''b'''nium, '''S'''eabor'''g'''ium, '''B'''o'''h'''rium, '''H'''a'''s'''sium, '''M'''ei'''t'''nérium, '''D'''arm'''s'''tadtium, '''R'''oent'''g'''enium, '''C'''oper'''n'''icium * « Les '''Fr'''ancais '''Ra'''lent '''Ac'''tivement depuis que '''R'''a'''f'''farin a '''D'''ou'''b'''lé '''S'''é'''g'''olène, é'''B'''a'''h'''ie par son '''H'''i'''s'''toire '''M'''ue'''t'''te sur la '''D'''i'''s'''tribution '''R'''é'''g'''ionale de la '''C'''o'''n'''nerie. » * « '''Fr'''anck '''Ra'''te '''Ac'''tuellement le '''R'''on'''f'''lement '''D'''é'''b'''ile du '''S'''i'''g'''nal '''B'''o'''h'''émien à l''''H'''i'''s'''toire '''M'''y'''t'''hique... » ===== [[w:Lanthanides|Lanthanides]] ===== ''Pour : ('''La'''nthane), '''Cé'''rium, '''Pr'''aséodyme, '''N'''éo'''d'''yme, '''P'''ro'''m'''ethium, '''S'''a'''m'''arium, '''Eu'''ropium, '''G'''a'''d'''olinium, '''T'''er'''b'''ium, '''Dy'''sprosium, '''Ho'''lmium, '''Er'''bium, '''T'''hulliu'''m''', '''Y'''tter'''b'''ium, '''Lu'''técium'' * « '''Cé'''dric, '''Pr'''ophète '''N'''éan'''d'''ertalien, '''P'''ro'''m'''et la '''S'''a'''m'''ba. '''Eu'''gène, '''G'''ran'''d''' '''T'''rou'''b'''le '''Dy'''namique des '''Ho'''mmes '''Er'''rants, '''T'''o'''m'''be sur l'h'''Yb'''ride '''Lu'''ne. » * « La '''Ce'''llule du '''Pr'''ofesseur est à '''N'''otre-'''d'''ame de '''P'''ana'''m'''e, '''S'''o'''m'''met de l' '''Eu'''rope, '''G'''ran'''d'''e et '''T'''rès''' b'''elle, où '''Dy'''onisos et '''Ho'''mère '''Er'''raient sans '''T'''u'''m'''ulte. '''Y'''a'''b'''on '''Lu'''tèce! » * « '''Ce'''cile, qui '''Pr'''atiquait le '''N'''u'''d'''isme, '''P'''ro'''m'''ettait à la '''S'''a'''m'''aritaine '''Eu'''phorique un '''G'''o'''d'''emichet avec un '''T'''u'''b'''e de '''Dy'''namite, '''Ho'''chet '''Er'''otique et '''T'''u'''m'''éfiant, s'''Y'''m'''b'''ole de '''Lu'''xure. » * « '''Ce'''sar se '''Pr'''omène avec '''N'''a'''d'''ine et '''P'''a'''m'''ina, en '''S'''e'''m'''ant les '''Eu'''nuques qui '''G'''ar'''d'''aient le '''T'''éné'''b'''reux '''Dy'''lan dans un cac'''Ho'''t car il '''Er'''rait près de la '''T'''o'''m'''be d''''Yb'''-'''Lu'''" * « '''Ce''' '''P'''a'''r'''adis que '''N'''ous '''d'''onna ('''Nd''') '''P'''ro'''m'''éthée, '''S'''e'''m'''blable à l''''Eu'''rope, nous '''G'''ar'''d'''e des '''T'''erri'''b'''les '''Dy'''sputes à l''''Ho'''rizon. Nous '''Er'''igerons une '''T'''o'''m'''be et '''Y''' '''b'''annirons '''Lu'''cifer » ===== [[w:Actinides|Actinides]] ===== ''Pour : ('''Ac'''tinium), '''Th'''orium, '''Pr'''otactinium, '''U'''ranium, '''N'''e'''p'''tunium, '''P'''l'''u'''tonium, '''Am'''ericium, '''C'''uriu'''m''', '''B'''er'''k'''élium, '''C'''ali'''f'''ornium, '''E'''in'''s'''teinium, '''F'''er'''m'''ium, '''M'''en'''d'''élénium, '''No'''bélium, '''L'''aw'''r'''encium.'' * « '''Th'''éo '''Pa'''rle '''U'''niversellement mais ''' N' '''ex'''p'''rime '''P'''l'''u'''s l''''Am'''ertume '''C'''o'''m'''mune. '''B'''roo'''k''' '''C'''on'''f'''ie l''''Es'''poir de '''F'''or'''m'''er un '''M'''on'''d'''e '''No'''uveau et '''L'''ib'''r'''e. » * « L''''Ac'''tivation''' Th'''ermique des '''Pa'''tates à l''''U'''ranium du '''N'''é'''p'''al en '''Pu'''rée '''Am'''ène, '''C'''o'''m'''me à '''B'''ang'''k'''ok, le '''C'''on'''f'''ort '''Es'''thétique d'une '''F'''a'''m'''ine un '''M'''i'''d'''i de '''No'''ël au '''L'''ibé'''r'''ia. (ou en bord de '''Lw'''oire selon les classifications) » * « '''Th'''or '''Pa'''rtit '''U'''ne '''N'''uit '''p'''our '''P'''l'''u'''ton, '''Am'''oureux de '''C'''a'''m'''ille. '''B'''er'''k'''eley, '''C'''ali'''f'''ornia '''Es'''perait '''F'''u'''m'''er '''M'''a '''d'''ouce et '''No'''ble '''L'''au'''r'''a.» ===== [[w:Alcalin|Alcalin]]s Groupe 1 ===== ''Pour : ('''H'''ydrogène, non alcalin), '''Li'''thium, '''Na'''trium (Sodium), '''K'''allium (Potassium), '''R'''u'''b'''idium, '''C'''é'''s'''ium, '''Fr'''ancium.'' * « ('''H'''eureux) dans le '''Li'''t de '''Na'''tacha, [[w:Khrouchtchev|'''K'''hrouchtchev]] '''R'''a'''b'''aissait '''C'''on'''s'''tamment son '''Fr'''oc. » * « '''L'''’'''i'''nter'''Na'''tionale '''K'''ommuniste '''R'''e'''b'''ute les '''C'''apitali'''s'''tes '''Fr'''ançais. » * « '''Li'''li '''N’a''' '''K''' '''R'''e'''b'''outonner '''C'''e'''s''' '''Fr'''ocs ('''Fr'''usques). » ===== [[w:Alcalino-terreux|Alcalino-terreux]] Groupe 2 ===== ''Pour : '''Bé'''ryllium, '''M'''a'''g'''nésium, '''Ca'''lcium, '''S'''t'''r'''ontium, '''Ba'''ryum, '''Ra'''dium.'' * « '''Bé'''bel(mondo) '''M'''an'''g'''eait du '''Ca'''ssoulet '''S'''u'''r''' un '''Ba'''teau '''Ra'''pide. » * « '''Bé'''bert '''M'''an'''g'''ea du '''Ca'''nard '''S'''u'''r''' un '''Ba'''teau-'''Ra'''dar. » * « '''Bé'''ta '''M'''an'''g'''ea du '''Ca'''ca '''S'''u'''r''' le '''Ba'''r de '''Ra'''bat (Maroc). » * « '''Bé'''atrice '''M'''an'''g'''ea une '''Ca'''rotte en '''S'''i'''r'''otant un '''Ba'''nana-split '''Ra'''vissant. » ===== Groupe 13 ===== ''Pour '''B'''ore, '''Al'''uminium, '''Ga'''llium, '''In'''dium, '''T'''ha'''l'''lium.'' *"'''B'''oris '''Al'''lait '''Ga'''mbader '''In''' '''T'''ou'''l'''ouse" ===== [[w:Cristallogène|Cristallogène]]s Groupe 14 ===== ''Pour : '''C'''arbone, '''S'''ilicium, '''Ge'''rmanium, '''S'''ta'''n'''num (Étain), '''P'''lom'''b'''.'' * « '''C'''es '''S'''imples '''Ge'''stes '''S'''eraie'''n'''t '''P'''ro'''b'''lématiques'''. »''' * « '''C''' 'est '''Si''' '''Gê'''nant '''S'''a'''n'''s '''P'''u'''b'''is. » ===== [[w:Pnictogène|Pnictogène]]s Groupe 15 ===== ''Pour : '''N'''itrogène (Azote), '''P'''hosphore, '''A'''r'''s'''enic, '''S'''ti'''b'''ium (Antimoine), '''Bi'''smuth.'' * « '''N'''e '''P'''as '''As'''tiquer '''S'''e'''b''' et sa '''Bi'''te. » * « '''N'''e '''P'''as '''As'''tiquer '''S'''o'''b'''rement le '''Bi'''zuth. » * « '''N'''e '''P'''as '''As'''tiquer le'''S b'''outs de '''Bi'''te. » * « '''N'''e '''PAs''' '''S'''a'''b'''rer Byzance('''Bi'''). » * " '''N'''e '''P'''as '''As'''soir '''S'''a'''b'''rina '''Bi'''zarrement" ===== [[w:Chalcogène|Chalcogène]]s Groupe 16 ===== ''Pour : '''O'''xygène, '''S'''oufre, '''Sé'''lénium, '''Te'''llure, '''Po'''lonium.'' * « '''O'''live '''S'''uce le '''Se'''xe '''Te'''ndu de '''Po'''peye. » * « '''O'''hh '''S'''uce moi le '''Se'''xe et les '''Te'''sticules '''Po'''ilus. » * « '''O'''scar '''S'''uce '''Se'''s '''Te'''sticules '''Po'''ilus. » * « '''O'''rgasme '''S'''ur le '''Se'''duisant '''Te'''odore '''Po'''ilu. » * « '''OS''' '''Se'''dimentaire '''Te'''rriblement '''Po'''li. » * « '''O'''h '''S'''acré '''Se'''igneur aux '''Te'''sticules '''Po'''lyèdriques. » * « '''O'''h '''S'''eigneur '''Sé''' (c'est) '''Te'''llement '''Po'''urri. » ===== [[w:Halogène|Halogène]]s Groupe 17 ===== ''Pour : '''F'''luor, '''C'''h'''l'''ore, '''Br'''ome, '''I'''ode, '''A'''s'''t'''ate.'' * « '''F'''ootball '''Cl'''ub de '''Br'''èles '''I'''ncapables d''''At'''taquer. » * « '''F'''ranck et '''Cl'''aude '''Br'''outent '''I'''rène '''A''' '''t'''able. » * « Une '''F'''issure '''Cl'''aviculaire '''Br'''isa tout '''I'''ntérêt d''''At'''taquer. » * « '''F'''outez '''Cl'''aire, qui '''Br'''anle '''I'''saac, car elle '''At'''tend. » * « '''F'''antastique, '''Cl'''aire '''Br'''anche '''I'''nstinctivement l''''At'''tache. » * « '''F'''erdinand '''Cl'''aque '''Br'''utalement '''I'''rène '''A''' '''t'''erre. » * « Les '''F'''ameuses '''Cl'''ochettes des '''Br'''ebis d''''I'''talie '''At'''tirent. » * « Le '''F'''ranc '''Cl'''ovis '''Br'''oie d''''I'''nnombrables '''At'''omes. » * « '''F'''élicie '''Cl'''aqua '''Br'''ian, '''I'''nnocent '''At'''tardé. » * « '''F'''olle '''Cl'''ara '''Br'''ave l''''I'''nvincible '''At'''hena. » ===== [[w:Gaz noble|Gaz noble]]s Groupe 18 ===== ''Pour : '''Hé'''lium, '''Né'''on, '''Ar'''gon, '''Kr'''ypton, '''Xé'''non, '''R'''ado'''n'''.'' * « '''He'''rcule '''Né'''gligea d’'''Ar'''racher le '''K'''o'''r'''sage de '''Xé'''na et '''R'''o'''n'''fla. » * « '''Hé''','''Né'''ron, '''Ar'''rête de '''Kr'''âner, '''Xé'''nophobe '''R'''i'''n'''gard ! » ===== [[w:Métalloïde|Métalloïde]]s ===== ''Pour : '''B'''ore, '''Si'''licium, '''Ge'''rmanium, Arsenic '''As''', Antimoine '''Sb''', '''Te'''llure et '''Po'''lonium'' *« '''B'''ob '''Si'''ffle son '''Ge'''t '''As'''sis avec '''S'''é'''b''' devant la '''Té'''lé '''Po'''lonaise. » ==== Couples acide/base ==== L’a'''c'''i'''d'''e '''c'''è'''d'''e un ou plusieurs protons tandis que la b'''a'''se c'''a'''pte un ou plusieurs protons. ==== Couples oxydant/réducteur ==== « Les électrons sont du côté de l'Occident. » (phonétiquement ''l’oxydant'') On peut également retenir que : **Ox Fixe, Red Cède (L'oxydant fixe des électrons, le réducteur en cède) ** l’oxyd'''ant''' est méch'''ant''' (il prend donc des électrons) ; ** le réduct'''eur''' a bon c'''œur''' (il donne donc des électrons). **L'oxydANT gagnANT, réductEUR donnEUR **Cap sur l'occident ! (L'oxydant '''cap'''te les électrons) **Notons aussi que l'oXydant aXepte (accepte) les électrons. Phrase qui marche à la fois pour les couples Acide/Base et Oxydant/Réducteur : L'Apéro gagne toujours ! ( A[cide] perd (des protons), O[xydant] gagne (des électrons) ). '''<nowiki/>''' ==== Ordre de priorité des groupements radicaux dans la nomenclature==== '''<nowiki/>''' '''A'''bruti '''H'''ans '''est''' l' '''ami de''' '''Nitr'''o! '''Al'''lez '''c'''hantons, L''''alcool''' '''am'''ené '''i'''ci '''e'''st '''t'''rès '''t'''errible Acide carboxylique, Halogénure, Ester, Amide, Nitrile, Aldéhyde, Cétone, Alcool, Amine, Imine, Ether, Thiol, Thioléter (pour ces deux derniers se reporter à la longueur). '''A Carbalo''' '''Ester''' '''a mit''' du '''Nitrile Aldéhyde''', '''s'étonne''' '''Alcolaminimine'''. '''Et tertio''', du '''thioleter'''. "L'''oïc''' '''est''' '''l’ami de Dalton":''' acide carboxylique (-oïque), ester, amide, aldéhyde, cétone. (ne pas confondre l'aldéhyde et l'alcool- voir la longueur des mots: c'est le plus long qui gagne). '''<u>Règle de Cahn, Ingold et Prelog</u>''' <u>''pour '''I '''> '''Br''' > '''Cl''' > '''S''' > '''F '''> '''O''' > '''N''' > '''C''' > '''H'''''</u> ** « '''Ib'''ra '''Cl'''ame '''S'''a '''F'''oi '''O''' '''N'''ouveau '''C'''avani '''H'''éroïque. » === Thermodynamique === ==== Loi des gaz parfaits ==== '''pV''' = '''nRT''' '''p''' = pression en pascals ; '''V''' = volume en mètres cubes ; '''n''' = quantité de matière en mols ; '''R''' = constante des gaz parfaits. R = 8,3 J.K-1.mol-1 ; '''T''' = température en Kelvins '''P'''ascal '''v'''oulut '''n'''ous '''r'''endre '''t'''héiste (référence au pari de Pascal) '''P'''a'''v'''a'''n'''e'''r'''ai'''t''' (sans les voyelles) '''pV''' = '''nRT''' n’est pas pété, énervé (ptnrv) '''P'''rocès-'''v'''erbal ; '''n'''ous '''r'''end '''t'''riste '''P'''uissance de '''V'''itesse = '''n'''otion de '''R'''apidité '''T'''errestre - Pour les joueurs de jeu de rôle uniquement ! Les PV d'un Pokemon est égale au Niveau fois sa RésisTance <br> ==== Différentielle de l’enthalpie ==== dH = TdS + VdP '''d'''îners '''H'''onorables = '''T'''artes '''d'''e '''S'''aison + '''V'''ins '''d'''u '''P'''ays (dH=TdS + VdP) '''d'''ouces '''H'''armonies = '''T'''oniques '''d'''e '''S'''olfège + '''V'''ibrations '''d'''e '''P'''iano (dH=TdS + VdP) '''d'''écouvertes '''H'''éroïques = '''T'''résors '''d'''e '''S'''able + '''V'''oyages '''d'''e '''P'''irates (dH=TdS + VdP) '''d'''anses '''H'''ispaniques = '''T'''angos '''d'''e '''S'''eville + '''V'''alses '''d'''e '''P'''ampelune (dH=TdS + VdP) '''d'''épart '''H'''éroïque = '''T'''oujours '''d'''u '''S'''tyle + '''V'''itesse '''d'''e '''P'''ointe (dH=TdS + VdP) '''d'''estination des '''H'''istoriens = '''T'''raversant '''d'''es '''S'''iècles + '''V'''oyageant '''d'''ans le '''P'''assé (dH=TdS + VdP) ==== Différentielle de l’enthalpie libre ==== dG = VdP-SdT '''V'''iande '''d'''e '''P'''orc '''-''' '''S'''el '''d'''e '''T'''able (VdP-SdT) '''V'''ends '''d'''u '''P'''ain sans (-) '''S'''ortir '''d'''e '''T'''on '''G'''îte (VdP - SdT = dG) ==== Différentielle de l’énergie interne (sans variation de quantité de matière) ==== dU=TdS-PdV '''T'''u '''d'''ois '''S'''avoir mais '''P'''as '''d'''e'''V'''iner (TdS-PdV) '''T'''out '''d'''e '''S'''uite '''Moins''' de '''P'''oints '''d'''e '''V'''ie '''T'''éter '''D'''u '''S'''el '''P'''endant '''D'''eux (ou '''D'''ix) '''V'''endredis. '''T'''rou '''d'''ans le '''S'''lip et '''P'''antalon '''d'''ans le '''V'''ent '''<nowiki>d'</nowiki>'''après '''U'''lysse = '''T'''outes '''d'''es '''S'''irènes mais '''P'''as '''d'''es '''V'''ampires <br /> ==== Différentielle de l’énergie interne (avec variation de quantité de matière) ==== dU=TdS-PdV+µdn '''T'''rop '''d'''e '''S'''avoir mais '''P'''as '''d'''<nowiki/>'en'''V'''ie c'est être '''nu''' '''d'''ans la '''n'''uit === Géologie === '''Les ères géologiques, du Quaternaire au Primaire (permettant de retenir les datations approximatives 7x60 + 3x40 Ma)''' '''Cénozoïque''' 60 (Quaternaire + Tertiaire) '''Crétacé''' 120 '''Jurassique''' 180 '''Trias''' 240 '''Permien''' 300 '''Carbonifère''' 360 '''Dévonien''' 420 '''Silurien''' 460 '''Ordovicien''' 500 '''Cambrien''' 540 '''C'''ite '''C'''e '''J'''oli '''T'''ruc '''P'''our '''C'''onnaître '''D'''es '''S'''iècles '''O'''rdonnés '''C'''orrectement ==== Niveaux de l'échelle chronologique géologique ==== '''Éo'''le '''ér'''adiqua les '''pe'''upliers '''ép'''uisés par l''''âge'''. * (éon, ère, période, époque, âge) ==== Les six périodes géologiques de l’ère primaire ==== ;Cambrien, Ordovicien, Silurien, Dévonien, Carbonifère, Permien. * '''''Cambr'''onne, l’'''ord'''urier,''' s’il eû'''t été '''dévo'''t, n’eût point '''carboni'''sé son '''pèr'''e'' * '''''Cambr'''onne '''ordo'''nna '''sil'''ence et '''dévo'''uement à ses '''car'''abiniers '''perm'''issionnaires'' * '''''Cambr'''onne '''aur'''ait, '''s'il eût''' été '''dévo'''t, '''carboni'''sé son '''pèr'''e'' * '''c-or-si-dé-ca-pé''' = Corps si décapés. * ''Le '''ca-or-sil-dé-ca-pe''' ='' Le Cahors, il décape. ==== Les trois périodes géologiques de l’ère secondaire ==== ;Trias, Jurassique, Crétacé. * '''T'''rois '''j'''ours '''c'''hacune. ==== Les cinq périodes géologiques de l’ère tertiaire ==== ;Paléocène, Éocène, Oligocène, Miocène, Pliocène. * '''Pâl'''e '''Et o'''bscène '''Au lit''', '''Mio''' se '''Plie au'''x scènes (de l'amour) ==== Stalactites et stalagmites ==== ''Les stalac'''t'''ites '''t'''ombent, les stalag'''m'''ites '''m'''ontent.'' ==== Géophysique ==== Formule pour la [http://fr.wikipedia.org/wiki/Anomalie_de_Bouguer correction gravitationnelle de Bouguer]: 2*π*h*ρ*G (G=constante gravitationnelle ρ=Masse volumique/Densité) "deux pies hachent Roger" 2 π h ρ G (ρ = "Rho", lettre grecque) ==== [[w:Échelle de Mohs|Échelle de Mohs]] ==== "'''T'''a '''G'''rosse '''C'''oncierge '''F'''olle d''''A'''mour '''O'''se '''Q'''uémander '''T'''es '''C'''aresses '''D'''ivines" "'''T'''oi '''G'''rand '''C'''hevalier, '''F'''uis '''A'''vec '''O'''rdre '''Q'''uand '''T'''on '''C'''œur '''D'''éfaille" "'''T'''rès '''G'''rand '''C'''hemin de '''F'''er '''A'''pache. '''O'''h ! '''Q'''uel '''T'''emps '''C'''e '''D'''imanche !" "'''T'''on '''G'''igolo '''C'''onte '''F'''leurette '''A''' '''(H)O'''rtense, '''Q'''ui '''T'''e '''C'''ocufie '''D'''iablement !" "'''T'''on '''G'''rand '''C'''ul '''F'''endu '''A''' une '''O'''uverture '''Q'''ue '''T'''u '''C'''aches '''D'''écemment" "'''T'''on '''G'''ros '''C'''ochon '''F'''ait '''A'''ïe '''O'''uille '''Q'''uand '''T'''u '''C'''ognes '''D'''essus" ([[w:Talc|'''T'''alc]], [[w:Gypse|'''G'''ypse]], [[w:Calcite|'''C'''alcite]], [[w:Fluorite|'''F'''luorite]], [[w:Apatite|'''A'''patite]], [[w:Orthose|'''O'''rthose]], [[w:Quartz (minéral)|'''Q'''uartz]], [[w:Topaze|'''T'''opaze]], [[w:Corindon|'''C'''orindon]], [[w:Diamant|'''D'''iamant]]) === Botanique === ==== Distinguer les hêtres des charmes ==== Le '''charme''' d''''Adam''' est d''''être''' à '''poil'''. ou encore: "Être à poils charme Adam" La feuille du charme a des dents (charme d’Adam) et la feuille du hêtre a des poils (être à poil). ==== Distinguer les principales espèces de pin ==== Les aiguilles du pin '''blanc''' sont groupées par '''5'''. '''Blanc''' a '''5''' lettres. Le pin '''rouge''' a des aiguilles groupées par '''2'''. Le mot '''rouge''' a '''2''' syllabes (s'il est suivit d'un mot commençant par une consonne en versification). Les aiguilles du pin '''noir''' sont en groupes de '''2'''. Dans le mot '''noir''', il y a '''2''' voyelles. ==== Distinguer les sapins des épicéas ==== Les sapins ('''''A'''bies'') ont des cônes '''a'''scendants, les épicéas ('''''P'''icea'') ont des cônes '''p'''endants. ==== Distinguer les cèdres ==== Le cèdre de l’'''A'''tlas a les pointes des branches '''a'''scendantes, le cèdre de l’Himalaya (''Cedrus '''d'''eodara'') '''d'''escendantes, le cèdre du '''L'''iban horizonta'''l'''es. ==== Distinguer les platanes des érables ==== pl'''A'''t'''A'''ne : feuilles '''A'''lternes '''É'''rable : feuilles oppos'''É'''es ==== Distinguer un Catalpa d’un Paulownia ==== P'''a'''ulowni'''a''' : deux feuilles par nœud (2 fois le a dans le nom) C'''a'''t'''a'''lp'''a''' : trois feuilles par nœud (3 a dans son nom) ==== Distinguer les feuilles de trèfle ==== Les feuilles du trèfle blanc ont de petites dents autour, celles du trèfle rouge ont des poils autour. Dents blanches, poils rouges (et non dents rouges, poils blancs !) ==== Distinguer les feuilles de trèfles de celles des luzernes ==== Les luzernes (''Medicago'') ont des pointes (= aiguilles, les médecins font des injections) au bout des folioles. ==== Distinguer les vesces des gesses ==== gesses ('''''L'''athyrus'') : l'alignement des points d'insertion des filets des étamines forme un angle droit avec le tube des étamines → L vesces ('''''V'''icia'') : il est oblique par rapport au tube ; on retrouve ce côté oblique dans la lettre V ==== Distinguer les knauties des scabieuses ==== '''k'''nautie : 4 ('''k'''atr’) pétales dans chaque fleur de l’inflorescence '''s'''cabieuse : 5 ('''s'''inq) pétales par fleur de l’inflorescence et des '''s'''oies sur le réceptacle ==== Distinguer les plantules de céréales dans un champ ==== BOAS : le '''b'''lé étant plus riche a des oreillettes, des poils et une ligule l’'''o'''rge a des oreillettes et une ligule l’'''a'''voine a une ligule le '''s'''eigle étant plus pauvre, n’a plus rien === Zoologie === ==== Ordre des cétacés ==== « '''C'est assez''', dit la '''baleine''', al'''ors que''' j'ai le '''dos fin''' je me '''cache à l'eau''' **baleine, orque, dauphin, cachalot, mais la liste est très incomplète. ==== Ordre des tatous ==== Les tatous font partie de l’ordre des ''Édentés'' car : "T’as tout sauf les dents !" === Biologie === ==== L'ordre hiérarchique de la classification de [[w:Taxinomie|taxinomie]] ==== Des Rats Essayent de Courir là Où Finissent les Grands Espaces (Raccourcis).<br /> DoRs EnCOre, la Famille GÈRe.<br /> Reste En Classe Ou Fais Grandes Études.<br /> Reste En Contact, Odile, Fais Gaffe, Émile ! (inspiré de la Cité de la peur, où Odile est attachée de presse et Émile tueur)<br /> Domaine, Règne, Embranchement, Classe, Ordre, Famille, Genre, Espèce, (Race). RECOFGE: Règne, Embranchement, Classe, Ordre, Famille, Genre, Espèce ==== Les [[w:bases azotées|bases azotées]] de l’ADN ==== '''À''' '''T'''on '''G'''rand '''C'''œur. '''A'''h '''T'''a '''G'''ueule '''C'''rétin '''À''' '''T'''able '''G'''rand '''C'''hef ('''ATGC''' : [[w:adénine|adénine]], [[w:thymine|thymine]], [[w:guanine|guanine]], [[w:cytosine|cytosine]]) ==== Intron/Exon ==== '''Int'''ron = '''Int'''rus ou '''int'''rusif, c'est la partie de nucléotide d'un gène qui est excisé de l'ARN lors de l'épissage, à l'inverse des '''exons.''' '''<nowiki/>''' ==== La séquence nucléotidique des télomères humains ==== '''T'''ous '''t'''es '''a'''mis se '''g'''avent de '''g'''énial '''g'''uarana. (TTAGGG) ==== Les différentes phases de la [[mitose]] ==== **le Prophète Athée (Pro Met A T) ** Je te '''ProMets''' de l''''Ana'''l au '''Telo''' ** '''ProMets''' à '''Anna''' de '''Tél'''éphoner ** '''P'''etit '''M'''ammifère '''À''' '''T'''éton ** '''P'''etit '''M'''atin '''A'''uprès de '''T'''oi ** '''P'''etit '''M'''artien '''A'''ttaque la '''T'''erre ** '''P'''etite '''M'''émé '''A''' '''T'''éléphoné ** '''P'''apa '''M'''aman '''A'''mour '''T'''oujours ** '''P'''our '''M'''on '''A'''mi '''Th'''omas ** '''P'''our '''M'''on '''A'''mour '''T'''oujours ** '''P'''our '''M'''on '''A'''nus '''T'''roué ** '''P'''etite '''M'''ademoiselle '''A'''ge '''T'''endre ** Promettante : '''Pro'''/'''met'''t/'''an'''/'''te''' ** '''PROMETANATELO''' ** ou ProMéthée est AnaTello (Se rappeler de la phrase Prométhée est un intello) ** '''P'''apa '''M'''ange '''A''' '''T'''able ** '''P'''ro des '''M''' '''A''' '''T'''hs ** '''P'''rof de '''M''' '''A''' '''T'''hs ** '''P'''ierre '''M'''angea des '''A'''nanas '''T'''ransgéniques ** '''P'''ouvoir '''M'''asculin '''A'''vant '''T'''out ** '''P'''aris-'''M'''arseille '''A''' '''T'''rotinette ** '''P'''our '''M'''émoriser : '''A'''voir '''T'''ravaillé ** '''P'''our '''M'''ieux '''A'''pprendre'''T'''out ** '''P'''ays les '''M'''oins '''avancés''' ** Le '''Pro'''f '''Met''' l' '''Âne''' devant la '''Tél'''é ('''P'''rophase, '''M'''étaphase, '''A'''naphase, '''T'''élophase) ** '''P'''auline '''M'''arche '''à''' la '''T'''équila ** TAMPI (à lire à l'envers) ** c'est PRoMEtteur An(un) inTello ** Papa Mange un Abricot Trop sucré (avec le s de sucré pour la synthèse qui suit la mitose G1 --> S --> G2) ==== Les différentes phases de la PROPHASE ==== '''Le''' '''Zi'''zi du '''Pachy'''derme a des '''Di'''mensions '''Dia'''boliques. (Leptotène, Zygotène, Pachytène, Diplotène, et Diacinèse) ou Letzplin (lepto/zygo) protége (pachy) didier (diplo/diacinèse) ou Le Zip à Didier ou Le zizi n'a pas de diarrhée ou Le zizi poilu du doyen ou Le zizi du pachyderme et du diplodocus sont différents ou Les Zizis Peuvent Devenir Durs ! ou '''Pré'''férer '''Le''' '''Zi'''zi du '''Pachy'''derme à celui du '''Diplo'''docus '''Dia'''bétique Pour préleptotène, leptotène, zygotène, pachytène, diplotène, diacinèse ==== Les acides aminés dits essentiels : ==== ''on compte neuf acides aminés essentiels : le tryptophane, la lysine, la méthionine, la phénylalanine, la thréonine, la valine, la leucine, l'isoleucine et l'histidine'' Le (LEU) trou (THR) de l'hystérique (HIS) Lyse (LYS) fait (PHE) tripper (TRY) valentin (VAL) mais (MET) ilose (ILE) pas ! '''''Le''' '''très''' '''ly'''rique '''Tri'''stan '''fait''' '''va'''chement '''m'''archer '''Ys'''eult, quelle '''Hist'''oire !'' ou encore : Hystérique, le très lyrique Tristan fait vachement méditer Iseult en Argentine (His)Leu-Thr-Lys-Trp-Phe-Val-Met-Iso(Arg): Histidine et Arginine seulement essentiels chez les enfants. ('''Le'''ucine, '''Thré'''onine, '''Ly'''sine, '''Try'''ptophane, '''Phé'''nylalanine, '''Va'''line, '''M'''éthionine, '''Is'''oleucine, '''Hist'''idine) Met le dans la valise, il fait trop d'histoire avec l'argent/en argentine. le cours d’'''hist'''oire, '''il''' '''le''' '''lit''' '''mais''' '''fait''' '''tres''' '''trivial''' ('''Hist'''idine; '''Ile''': Isoleucine, '''Leu'''cine, '''Ly'''sine, '''Mé'''thionine, '''Phé'''nylalanine '''Thré'''onine,'''Try'''ptophane,, '''Va'''line) Dans une '''V'''(aline)'''I'''(soleucine)'''L'''(eucine), il y a des '''H'''(ystidine)'''L'''(ysine)'''M'''(ethionine) et des '''P'''(hénilalanine)'''T'''(hréonine)'''T'''(ryptophane) (Dans une ville, il y a des HLM et des PTT) ''ils le valent trop trop mes félicitations'' '''ile''' '''leu''' '''val''' '''thr'''''op'' '''tr'''''o'''''p''' '''met''' '''phe''' '''lys''' ''itations'' ''Va te le mettre, Phillipe'' '''VA'''l '''TH'''r '''LE'''u '''MET''' '''TR'''p '''PH'''e '''ILE''' '''LY'''s pe '''''Va''' '''tri'''poter '''Lys'''e mais ('''met''') fait ('''phe''') '''le''' '''tr'''ès '''iso'''lément'' ''val thr lys met phe (fait) leu trp ile (iso-leucine)'' Plus simple et plus concret que tous les autres moyens mnémotechniques: VTT MILLPH (prononcé VTT MILF) et ainsi vous obtiendrez : Valine, Thréonine, Tryptophane, Méthionine, Isoleucine, Leucine, Lysine, Phénylalanine, Histidine. ==== Les acides aminés dits apolaire (Proline polaire/apolaire comprise) ==== Valérie promet à la triste Iseult le phénix et la Glycine. (val) (Pro/Met)(Ala)(Trp) (Ile) (Leu) (Phe) (Gly) Glycine dévale à la pelle, il le promet trop. ==== Le [[w:cycle de Krebs|cycle de Krebs]] ==== **''Si le '''citr'''on '''iso'''<nowiki>le l'</nowiki>'''acéto'''ne, le '''succi'''nct '''succès''' '''fumera''' '''m'''oins '''haut''''' ('''citr'''ate, '''iso'''citrate, alph'''acét'''oglutarate, '''succ'''inyl CoA, '''succ'''inate, '''fumara'''te, '''ma'''late, '''o'''xaloacétate) ** Avec les initiales : "C'est ici ce samedi soir : fumette, mal-à-la-tête, oubliette." ou encore : ** ''La '''C''' '''I''' '''A''' '''su'''specte un '''su'''spect qui '''fum'''e des '''Mal'''boros '''ox'''ydées.'' ** '''O'''h '''C'''atastrophe ! '''I'''l '''Os'''e '''Ac'''tiver '''Sa''' '''Su'''per '''F'''orce '''M'''agique ==== Le [[w:Cycle de Calvin|Cycle de Calvin]] ==== **"Les '''ri'''mes '''intermédiaires''' aux '''fo'''rmes '''diffo'''rmes de '''PGAL''' '''ri'''ment." ('''ri'''bulose phosphate, '''intermédiaire''' instable qui se scinde en deux 3-'''pho'''sphoglycérate, 1,3-'''dipho'''sphoglycérate, phosphoglycéraldéhyde ('''PGAL'''), dont l'un quitte le cycle et cinq sont utilisés pour reformer le '''ri'''bulose phosphate.) ==== Les Aldohexoses ==== '''Allo'''ns, '''altr'''uiste '''gl'''acer la '''mann'''e, '''Gul'''liver '''i'''ra '''gal'''érer au '''tall'''us ('''Allo'''se; '''altr'''ose; '''gl'''ucose; '''mann'''ose, '''Gu'''lose '''i'''dose '''ga'''lactose '''ta'''lose) ==== Les protéines intervenant dans les jonctions cellulaires ==== (Attention que ces phrases ne fonctionnent pleinement que si l'on connaît <i>déjà</i> les protéines intervenant, mais que l'on a du mal à retenir lesquelles font quoi.) - Jonction Adherens : '''Vin'''t le '''cad'''avre '''é'''quipé d''''a'''rmes '''α''' qui '''plaqu'''a le '''glo'''ussant '''ca'''valier. -> '''Vin'''culine, '''cad'''hérine-'''E''', '''a'''ctine, '''α'''-actinine, '''plak'''o'''glo'''bine, '''ca'''ténine. - Jonction de contact focal : '''Vin'''t la '''paix''' '''intégr'''ale; les '''a'''rmes '''α''' en '''ta'''s. -> '''Vin'''culine, '''pax'''iline, '''intégr'''ines, '''a'''ctine, '''α'''-actinine, '''ta'''line. - Desmosome : Tout ce qui '''colle''', plus la '''kératine'''. -> Desmo'''coll'''ine, desmo'''glé'''ine, desmo'''plak'''ine, '''plak'''oglobine, '''plak'''ophiline, '''kératine'''. - Jonction Gap (de communication) : Elle induit une '''connexion'''. -> '''Connex'''ines. - Hémidesmosomes : La '''p'''yramide de '''Khé'''ops '''d'''étruit '''intégr'''alement '''la mi'''en'''ne'''. -> '''P'''lectine, '''ké'''ratine 5 et 14, '''d'''ystonine, '''intégr'''ine α6β4, '''laminine''' 332. - Jonction tight, ou étanche, qui comporte des "'''kissing''' points" et dont le complexe crée une "'''zonula occludens'''" : '''Embrasser''' '''Claudine''' crée une '''occlu'''sion '''acti'''ve. -> Claudine, occludine, actine, ZO-1. ==== Les protéines des filaments intermédiaires ==== Elles diffèrent en fonction du tissu où elles se trouvent. Attention que les moyens proposés ici servent plus à retrouver la fonction d'une protéine déjà connue qu'à retenir le nom en lui-même. - Épithéliums : Kératines. Facile, il suffit de réfléchir un instant pour s'apercevoir que l'épithélium est bourré de kératines (couche cornée, desmosomes, ...). - Tissu '''C'''onjonctif : '''V'''imentines. On retient "'''CV'''". - Tissu '''M'''usculaire : '''D'''esmines. On retient "'''MD'''", une abréviation fréquente en anglais pour qualifier un Docteur en Médecine (Medicinæ doctor). - Tissu Nerveux proprement dit : Protéines des neurofilaments. Elles n'ont donc pas de nom propre, leur nom est leur fonction : des '''protéines''' dans les '''filaments''' intermédiaires des '''neuro'''nes. - Tissu Nerveux "de soutien" (tissu glial, donc) : Protéines fibrillaires acides gliales. Elles n'ont pas de nom propre, le nom est la fonction : Des '''protéines''' qui génèrent des '''filaments''' ('''fibrillaires''', donc) appartenant au tissu '''glial'''. La seule chose à retenir est qu'elles sont acides. - Noyaux : Lamines. On peut retenir qu'elles forment la '''lamina''' nucléaire, ou encore que pour arriver au noyau d'une cellule il faut la "maltraiter", et pourquoi pas la '''laminer'''. ==Technologie== ===Électronique=== ====Code couleur des résistances==== Code couleur à retenir : Noir, Marron, Rouge, Orange, Jaune, Vert, Bleu, Violet, Gris, Blanc * Ne Mangez Rien Ou Je Vous Battrai Violemment Gros Béta. * Ne Mangez Rien Ou Je Vous Brûle Votre Grosse Barbe. * Ne Mangez Rien Ou Jeunez Voilà Bien Votre Grande Bêtise. === Informatique === ==== RJ45 croisé ==== Broches 361782'''45'''. ==== Modèle OSI ==== Le modèle OSI divise les fonctionnalités nécessaires à la communication en sept couches : *# '''P'''hysique, *# '''L'''iaison, *# '''R'''éseau, *# '''T'''ransport, *# '''S'''ession, *# '''P'''résentation, *# '''A'''pplication. ** Il faut être deux pour avoir une liaison. ** Le 4*4 est un transport. "Félicie, OSI" *#Séduit par son '''PHYSIQUE''' *#et n'ayant aucune '''LIAISON''', *#je l'ai contactée sur un '''RÉSEAU''' social. *#Arrivé chez elle en '''TRANSPORT''' en commun, *#suivi une '''SESSION''' de va-et-vient, *#sans aucune forme de '''PRÉSENTATION''', *#j'y ai mis toute mon '''APPLICATION'''. Le lendemain, elle me recontactait... Les mots des phrases suivantes ont des initiales identiques à celles des couches, dans l'ordre ci-dessus ( P L R T S P A ) : *#'''P'''artout '''L'''e '''R'''oi '''T'''rouve '''S'''a '''P'''lace '''A'''ssise *# '''P'''our '''L'''e '''R'''éseau '''T'''out '''S'''e '''P'''asse '''A'''utomatiquement *# '''P'''ar '''L'''à, '''R'''aisonnons '''T'''ransport '''S'''ans '''P'''résenter l''''A'''pplication *# ''Pour les amateurs du jeu d'échecs :'' '''P'''rends '''L'''a '''R'''eine '''T'''out '''S'''era '''P'''lus '''A'''gréable *# '''P'''etit '''L'''apin '''R'''ose '''T'''rouvé à la '''S.P.A.''' *# '''P'''our '''L'''e '''R'''epas '''T'''out '''S'''era '''P'''rêt '''À''' 7 heures (7 couches) *# '''P'''our '''L'''e '''R'''éseau '''T'''u '''S'''eras '''P'''as '''A'''ugmenté *# '''P'''our '''L'''a '''R'''oute, '''T'''u '''S'''uis '''P'''ierre-'''A'''lain ! *# '''P'''our '''L'''a''' R'''etenir '''T'''oujours '''S'''e '''P'''arler '''A'''vant ! Les mots des phrases suivantes ont des initiales identiques à celles des couches, dans l'ordre inverse ( A P S T R L P ) : *# '''A'''près '''P'''lusieurs '''S'''emaines, '''T'''out '''R'''espire '''L'''a '''P'''aix ==== Classe d'adresse IP ==== En binaire, compter le nombre de 1 avant le premier 0. * A : 0 -> 127 (+ 127) <code>0xxxxxxx</code> * B : 128 -> 191 (+ 63) <code>10xxxxxx</code> * C : 192 -> 223 (+ 31) <code>110xxxxx</code> == Grammaire et orthographe == ===<u>Les principaux mots interrogatifs</u>=== Ce moyen mnémotechnique est très utile pour les coups de téléphone où l’on doit demander des renseignements. Il faut dresser rapidement la liste des mots interrogatifs sur un papier et être sûr que l’on a des réponses à toutes les questions. '''C’est cucu, c’est occupé !''' *'''C'''ombien ? *'''Q'''uoi ? *'''Q'''ui ? *'''C'''omment ? *'''O'''ù ? *'''Q'''uand ? *'''P'''ourquoi ? === [[w:Conjonction de coordination|Conjonctions de coordination]] === ''Mais où est donc Ornicar ?'' (Mais, Ou, Et, Donc, Or, Ni, Car) Mais cette méthode est pédagogiquement discutable, car elle entretient la confusion entre ''et'' (conjonction) et ''est'' (verbe ''être'' à la troisième personne du singulier), ainsi qu'entre ''ou'' (conjonction) et ''où'' (adverbe ou pronom relatif). Attention, ''donc'' n’est plus une conjonction de coordination, mais bien un verbe conjugué pour ''est'' et un adverbe de coordination pour ''où'' ! On peut aussi l’apprendre de cette manière afin de sortir le OU et ne pas induire de confusion dans l’esprit Mais ! Et donc Ornicar (mais, et, donc, or, ni, car) en jouant sur la sonorité de la surprise Au Québec, on dit aussi: ''Mais où est donc Carnior ?'' ===<u>Les principales prépositions</u>=== *''Adam part pour Anvers avec cent sous sûrs, entre derrière chez Decontre'' :(À, Dans, Par, Pour, En, Vers, Avec, Sans, Sous, Sur, Entre, Derrière, Chez, De, Contre) *''Adam part pour envers avec deux cents sous chez Parmisur. '' :(À, Dans, Par, Pour, En, Vers, Avec, De, Sans, Sous, Chez, Parmi, Sur) *''Adam part pour Anvers avec deux cents sous.'' *"Adam part pour Anvers avec deux cents sous chez Sur." :( À, Dans, Par, Pour, En, Vers, Avec, De, Sans, Sous, Chez, Sur) *''Adam part pour Anvers avec cent sous de chez surdurand.'' *"(À, Dans, Par, Pour, En, Vers, Avec, Sans, Sous, De, Chez, Sur, Durant) *''Adam Surché part pour Anvers avec deux-cents sous'' *"(À, Dans, Sur, Chez, Par, Pour, En, Vers, Avec, Sans, Sous) *’’ Adeudans part pour Sur sans sous chez Devant-derrière avec Avant-après-contre’’ ===<u>Les pronoms relatifs</u>=== 3 culs domptent ouvertement monsieur lequel,Duquel,Auquel... ''<nowiki>Qui que quoi dont où lequel duquel auquel…'</nowiki>'' ===<u>Les déterminants possessifs</u>=== Au pluriel: ''Mais c'était nos voleurs !'' (mes, ses, tes, nos, vos, leurs) ===<u>Orthographe</u>=== * Mou'''r'''ir ne prend qu’un “ r ” car on ne meurt qu’une fois. * Nou'''rr'''ir prend deux “ r ” car on se nourrit plusieurs fois. * Cou'''r'''ir ne prend qu’un “ r ”car on manque d’air en courant,<br /> mais quand on a'''rr'''ive on prend tout l’air qu’on peut. * L’hironde'''ll'''e prend deux “ l ” car elle vole avec ses deux ailes. * La v'''i'''e'''i'''lle ne peut marcher qu’avec ses deux bâtons. * A'''pp'''uyer prend deux « p » car on s’appuie mieux sur deux pattes. * Un ba'''l'''ai prend un seul “ l ” car il n’y a qu’un manche. * Un ba'''ll'''et prend deux “ l ” car pour danser il faut deux jambes. * Toujour'''s''', toujours un “ s ” et à jamai'''s''', ne jamais l’oublier. * J’a'''p'''erçois sur une jambe mais j’a'''pp'''arais sur les deux. * Quand je mets deux "p" à apercevoir, j'aperçois une faute. * Je n’a'''p'''erçois qu’un '''p''' à a'''p'''ercevoir (ou je m’a'''p'''erçois qu’a'''p'''ercevoir ne prend qu’un '''p'''). * Enve'''l'''oppe ne prend qu’un “ l ” car on ne met qu’une lettre dans une enveloppe. En revanche pour un vélo on a deux '''p'''neus : dé''velo'''''pp'''er, en''velo'''''pp'''er, etc. * Cuiss'''eau''' de v'''eau'''. * Sate'''ll'''ite prend 2 '''L''' car c’est plus pratique pour voler. (et un seul '''t''' car il ne tourne qu’autour d’une seule '''T'''erre) * Évide'''mm'''ent prend deux '''m''' comme dans '''Papa/Maman''' (à noter : tous les adjectifs qui se terminent par "ent", comme "évident", prennent 2 "m" ensuite, comme "évidemment"). * Je me souviens d’une corde en rappel : On se souvient '''DE''' quelque chose, mais on se rappelle quelque chose. * Un pa'''r'''esseux cou'''r'''onné ca'''r'''essait une ca'''r'''otte avec un air intéressé : liste de mots qui ne prennent qu’un '''r'''. * Co'''ll'''ine a deux colonnes (2 ” l ”) et colo'''nn'''e a deux collines (2 ” n ”). * Dé'''velopp'''er je fais du vélo avec mes deux pieds pour pédaler. * Échapper prend deux "P" car on s'échappe mieux avec deux pieds. * Culo'''tt'''e prend deux '''T''' car il y a deux jambes pour une culotte * Un professeur a un seul '''F'''ront et deux '''S'''ourcils, donc un seul F, mais deux S * Philippe : je marche (2p) mais ne vole pas (1l) * On parle le flaman'''D''' dans les Flan'''D'''res. Le flaman'''T''' rose est un oiseau de grande '''T'''aille. * L’am'''a'''nde pousse sur un '''a'''rbre ; l’am'''e'''nde sur un '''e'''ssuie-glace. *Guè'''r'''e signifie "pas beaucoup", donc un seul r. Il faut au moins deux adversaires pour faire la gue'''rr'''e, donc 2 R. * Une P'''ê'''che (melba...) / P'''ê'''cher (du poisson) / P'''é'''cher (commettre une offense) / un P'''é'''ché (originel...) : Dans la p'''ê'''che en rivière, le '''^''' représente l’hameçon et la p'''ê'''che (fruit) représente le flotteur de la canne à p'''ê'''che). Quand on confesse au prêtre un p'''é'''ché, on fait profil bas (accent aigu sur le '''é'''). * Tous les membres de la famille ont un accent grave, sauf pépé et mémé : père, mère, nièce… * M devant Mbappé (M devant M, B et P) * Reg versus Erg : ** Un Reg est un désert de Roches, de pieRRes ** L’ERg est un désERt de dunes ===<u>Mots avec accent circonflexe</u>=== * Une t'''a'''che c'est suffisamment sale pour ne pas avoir besoin d'en rajouter (d'accent circonflexe) * Le chapeau de c'''i'''me est tombé dans l’ab'''î'''me. Et celui du bo'''i'''teux dans la bo'''î'''te ! * On dit chapeau ! pour la '''tâche''' accomplie et non pas chapeau ! pour la '''tache''' sur le vêtement. * Un chien ou un chat marche sur deux paires de pa'''tt'''es. Par contre, on fait cuire des p'''ât'''es dans une casserole qu'on couvre avec le chapeau du â. * "Traîner ses guêtres", c'est flâner. <u>Pour les anglophones</u>, il suffit souvent de comparer le mot anglais de même racine que le mot français sur lequel on a un doute pour l'accent circonflexe. Si ce mot anglais contient un S, le mot français équivalent contient souvent un accent circonflexe. Exemples : * Ancêtre / Ancestor * Apôtre / Apostle * Arrêt / Arrest * Bâtard / Bastard * Bête / Beast * Boîte / Box * Château / Castle *Cloître / Cloister * Côte (anatomie, rivage, pente, culinaire) / Coast (rivage) * Coût / Cost * Crête / Crest (vague, cimier, huppe…) * Dégoût / Disgust * Épitre / Epistle * Fête, Festif (fr) / Feast (eng) * Guêpe / Wasp (eng) tous deux venant de vespa (latin) * Forêt / Forest * Hâtif / Hasty * Hôpital / Hospital * Hôte, hôtesse / Host, hostess * Hâte / Haste * Honnête / Honest * Huître / Oyster * Île / Island * Intérêt / Interest * Maître / Master * Mât / Mast (bateau) * Paître / To pasture * Pâtisserie, Pâte / Pastry / Pasta (ital.) * Plâtre / Plaster * Quête / Quest * Rôtir / To roast * Tâche (travail et non salissure) / Task * Tempête / Tempest ===<u>Pluriel</u>=== *Pluriel en OUX au lieu de OUS ::Un '''hibou''' moche comme un '''pou''' ::Avait pour '''joujou''' sur ses '''genoux''' ::Un '''caillou''' aussi '''chou''' qu’un '''bijou'''. Variante : ::Viens mon '''chou''', mon '''bijou''' ::Viens sur mes '''genoux''' ::Avec des '''joujoux''' et des '''cailloux''' ::Pour éloigner ces vilains '''hiboux''' pleins de '''poux''' Variante : ::Viens mon '''chou''', mon '''joujou''', mon '''bijou''' ::Sur mes '''genoux''' ::Jeter des '''cailloux''' ::À ces vieux '''hiboux''', pleins de '''poux''' Variante :<blockquote>Viens mon '''chou''', sur mes '''genoux''' avec tes '''joujoux''' et tes '''bijoux'''</blockquote><blockquote>Pour jeter des '''cailloux''' sur les vilains '''hiboux''' pleins de '''poux'''.</blockquote> Variante : Répéter plusieurs fois très vite : Hi-ge-jou-bi-ca-chou-pou. Vous avez ainsi les premières syllabes des 7 noms qui se terminent en "oux" au pluriel. 2e variante: '''J'''e '''P'''eux '''B'''oire '''C'''omme '''C'''es '''G'''ros '''H'''ommes. * Les noms terminés en « -al » font leur pluriel en « -aux » (sauf ''aval, bal, cal, carnaval, chacal, choral, festival, mistral, naval, pal, récital, régal''… qui font leur pluriel en « s ») : ::Dans mon pays '''natal''' ::Où les gens sont pourtant '''joviaux''' ::Eut lieu, c’était '''fatal''', un combat '''naval''', ::Heureusement, ce fut le combat '''final''' ::Parce qu’il faisait '''glacial'''. == [[w:Grammaire|Langues étrangères]] == Ces langues nous sont étrangères, d’où l’importance de trouver des moyens mnémotechniques === [[w:Allemand|Allemand]] === ==== Liste des particules verbales non détachables ==== ''J’ai mis Cerbère en enfer'' : ge-, miss-, zer-, be-, er-, ent-, emp-, ver- ''Cerbère gémit en enfer'' : zer-, be-, er-, ge-, miss-, ent-, emp-, ver- ''Miss Verzer bégaie en panthère'' : miss-, ver-, zer-, be-, ge-, emp-, ent-, er- ==== Genre des mots ==== Les mots (de plus d'une syllabe) se terminant en -e, -ei, -ie, -heit, -keit, -tion, -ung sont féminins. <br /> Il existe bien sûr des exceptions : der Däne, das Genie, der Ursprung, der Hochsprung... === [[w:Anglais|Anglais]] === ==== Mots contraires ou confondables ==== * Left: gauche / '''R'''ight: d'''r'''oite ** avec la''' main gauche''', on peut former un '''L''' en tenant les doigts en haut et le pouce en avant. C’est le '''L''' de '''L'''eft. ** dans l’alphabet le '''L''' est à '''gauche''' ('''L'''eft) et le '''R''' est à '''droite''' ('''R'''ight) : **:A B C D E F G H I J K '''L''' M N O P Q '''R''' S T U V W X Y Z ** Copy'''right''' veut dire '''droit''' d'auteur. ** Quand on est a'''droit''', c’est bien (= '''right''' en anglais) * Odd (3 lettres) : impair / Even (4 lettres) : pair ** Tuesday : mardi / Thu'''r'''sday : jeudi *** Thu'''r'''sday est le quat'''r'''ième jour de la semaine, il a donc un '''r''' ('''quatrième''' lettre) *** En classant les mots dans l’ordre lexicographique : ****jeudi (Thursday) est avant mardi (Tuesday), ****Thursday (jeudi) est avant Tuesday (mardi). *** Je'''u'''di et Th'''u'''rsday ont tous les deux la lettre '''u''' en troisième position. *** étymologiquement : Thursday = jour de '''Thor''', et jeudi = jour de '''Jupiter''' (Jovis die). Thor (mythologie nordique) et Jupiter (mythologie romaine) sont tous les deux ''dieu du tonnerre''. Même chose pour l'étymologie de Tuesday (jour de '''Tyr''') et de mardi (jour de '''Mars'''), tous les deux étant ''dieu de la guerre''. Mais il est plus difficile de retrouver Odin dans Tuesday. ***TUEsday sonne comme two-sday two étant égal au nombre 2 et mardi est le deuxième jour de la semaine. === [[w:Espagnol|Espagnol]] === ==== [[w:Consonne|Consonnes]] doublées ==== Les seules consonnes que l’on peut trouver à l’écrit en double sont celles du mot CaRoLiNa. On peut remarquer que le "[[w:LL|ll]]" est une consonne à part entière. Attention ! Ne pas confondre N et Ñ ==== <u>Subjonctif</u> ==== ===== verbe Savoir (saber) ===== si tu '''sé '''ton présent mais que tu ne '''sepa '''ton subjonctif ce n'est pas grave! === [[w:Japonais|Japonais]] === {{article détaillé|Japonais/Hiragana/Leçon 1}} === [[w:Latin|Latin]] === ==== Ordre des six cas principaux du latin ==== '''No'''us '''Vo'''us '''Ac'''hetons '''Gé'''néralement '''D'''es '''Ab'''ricots '''No'''minatif, '''Vo'''catif, '''Ac'''cusatif, '''Gé'''nitif, '''D'''atif, '''Ab'''latif === [[w:Néerlandais|Néerlandais]] === ==== Liste des particules verbales non détachables ==== ''begeherontverer' : be-, ge-, her-, ont-, ver-, er-'' BEnoit et Gerard ONT HERité du VERgER ==== Conjugaison de l’[[w:imparfait|imparfait]] ==== On forme l’[[w:imparfait|imparfait]] avec un '''t''' si le radical (Verbe -EN) du verbe se termine par F, K, P, S, T, CH.<br> Retenez : '''F'''ran'''K'''lin '''p'''rend '''s'''on '''t'''hé '''ch'''aud. <br> Si le radical se termine par une autre lettre, on forme l’imparfait avec un '''d'''. Exemples : <br> - pakken (''prendre'') : pak'''k'''-en > hij pak'''t'''e (''il prenait'')<br> - ruilen (''échanger'') : rui'''l'''-en > hij ruil'''d'''e (''il échangeait'') === [[w:Russe|Russe]]=== ==== Verbes à voyelle alternante ы/о dans le thème ==== '''К'''арл '''Р'''о'''М'''а'''Н'''о'''В''' крыть « couvrir » - рыть « creuser » - мыть « laver » - ныть « gémir; faire mal » - выть « hurler » Se fléchissent tous sur le modèle : * infinitif : мыть (accent stable au passé: м'ыла) * conjugaison : мóю, мóешь... мóют Contrairement à ст'ыть, ст'ыну « refroidir » ; слыть, слывý « être réputé... » == Littérature == === Auteurs français du {{XVIIe siècle}} === ''Sur une racine de la bruyère, une corneille boit l’eau de la fontaine Molière'' ([[w:Jean Racine|Racine]], [[w:Jean de La Bruyère|Jean de La Bruyère]], [[w:Pierre Corneille|Pierre Corneille]], [[w:Nicolas Boileau|Nicolas Boileau]], [[w:Jean de La Fontaine|Jean de La Fontaine]], [[w:Molière|Molière]]) Variante : La Corneille perchée sur la Racine de La Bruyère, Boileau de La Fontaine Molière ''(Remarque : La fontaine Molière est une fontaine à Paris)'' == Théâtre == Face à la scène, le côté '''j'''ardin et le côté '''c'''our sont du côté de chaque initiale de '''J'''ésus '''C'''hrist, de '''J'''ules '''C'''ésar , de '''J'''acques '''C'''artier ou de '''J'''acques '''C'''hirac (J.C. : jardin à gauche, cour à droite) Face au public, c’est l’inverse, et le côté '''cour''' est le côté du [[w:cœur|cœur]], à gauche. == Musique == '''Ah ! Lala !''' * Se souvenir de cette interjection pour faire correspondre les notes musicales latines (do, ré, mi fa...) avec les anglo-saxonnes (C, D, E, F...) '''A''' correspond à '''la''', il n'y a plus qu'à suivre B=si, C=do, D=ré, E=mi, F=fa, G=sol. '''TS MS DSS''' *''Nom des degrés'': '''t'''onique, '''s'''us-tonique, '''m'''édiante, '''s'''ous-dominante, '''d'''ominante, '''s'''us-dominante, '''s'''ensible '''Sa mère la racaille ! Saleté de fumier !''' *''Ordre des bémols'' : '''S'''i '''m'''i '''l'''a '''r'''é '''s'''ol '''d'''o '''f'''a '''Six mille laquais repus songent au dodo, fatigués''' *''Ordre des bémols'' : '''Si''' '''mi''' '''la''' '''ré''' '''so'''l '''do''' '''fa''' '''Facteur, donne au soldat réjoui la missive''' *''Ordre des dièses'' : '''Fa''' '''do''' '''sol''' '''ré''' '''la''' '''mi''' '''si''' '''Dommage, la mine est cassée Do Majeur --> La mineur''' *"Gamme relative" de Do Majeur '''Il Doit Posséder Les Modes En Lui''' Ionien, Dorien, Phrygien, Lydien, Mixolydien, Éolien, Locrien == Géographie == === Points cardinaux === ==== Où est l’est ? ==== *Visualiser Strasbourg et Brest. Strasbourg est à l'est, Brest est à l'ouest * Penser au mot '''O'''rang'''E''', sur une carte, l'Ouest est à gauche et l'Est à droite. * Écrire "'''où est''' l''''est'''" --> ouest à gauche et est à droite (en considérant le nord en haut bien entendu) * L’ouest est à gauche, l’est à droite (si le nord est au-dessus). * Penser que si on regarde la France, à gauche c'est l' ''eau'' comme dans '''O'''uest et à droite c'est l' ''étranger'' comme dans '''E'''st. * Penser qu'en France, on parle des "pays de l'est" (à droite sur la carte) et en parlant de la conquête de l'ouest on pense à l'Amérique (à gauche sur la carte) * Penser à : Ouest, le suffixe "est" se trouve à droite, tout comme l’est. Ce qui signifie "ouest" à gauche et "est" à droite. * Écrire ONE (1 en anglais) : Ouest-Nord-Est * Penser au mot 'OiE': l’Ouest est à gauche comme le O et l’Est est à droite comme le E (si le Nord est en haut) * Dans le mot ouest il y a un "u" comme dans gauche. Dans le mot est il n'y a pas de "u" comme dans droite. * Retenir le mot NESO en tournant dans le sens des aiguilles d'une montre, car l'inverse donne la nausée (NOSE). ==== Le soleil se couche à l’est ou à l’ouest ? ==== * Penser qu’en France, on voit de beaux couchers de soleil sur nos côtes atlantiques, à l’ouest. * Penser aussi au pays du soleil levant, le Japon, qui est bien à l’est du continent * Ou encore : Le Soleil se l'''è'''ve à l’'''e'''st et se c'''ou'''che à l’'''ou'''est === Pays limitrophes de la France === Aime '''I''S''A''B''E''L''A''' (Aimer pour la lettre '''M''') '''M'''onaco, '''I'''talie, '''S'''uisse, '''A'''llemagne, '''B'''elgique, '''E'''spagne, '''L'''uxembourg, '''A'''ndorre. ''MAL BAISÉ'' '''M'''onaco, '''A'''ndorre, '''L'''uxembourg, '''B'''elgique, '''A'''llemagne, '''I'''talie, '''S'''uisse, '''E'''spagne. ''AIMABLES'' '''A'''llemagne, '''I'''talie, '''M'''onaco, '''A'''ndorre, '''B'''elgique, '''L'''uxembourg, '''E'''spagne, '''S'''uisse Avec l’océan Atlantique, la Manche et la Méditerranée en plus : '' '''O'''h '''MA''' '''MER'''veilleuse '''BALISE''' '' '''O'''h = '''O'''céan Atlantique '''MA'''='''MA'''nche '''MER'''= '''MER'''Méditerranée '''BALISE'''= '''B'''elgique '''A'''llemagne '''L'''uxembourg '''I'''talie '''S'''uisse '''E'''spagne PS cette liste est valable seulement pour la France Métropolitaine car le pays avec lequel la France a la plus longue frontière est le ... Brésil ! (car la Guyane est un département français) === Grands lacs de l'Amérique du Nord === ''SMHEOL'' (d'ouest en est) '''S'''upérieur, '''M'''ichigan, '''H'''uron, '''E'''rié, '''O'''ntario (et St '''L'''aurent) ''HOLMES (élémentaire !)'' '''H'''uron, '''O'''ntario, St '''L'''aurent, '''M'''ichigan, '''E'''rié, '''S'''upérieur. Une autre méthode, souvent enseignée dans les cours de géographie, fait appel au mot anglais ''foyers'' de la manière suivante : ''HOMES'' '''H'''uron, '''O'''ntario, '''M'''ichigan, '''E'''rié, '''S'''upérieur === Les 5 arrondissements de New-York (Boroughs) === Vous n'arrivez pas à vous souvenir des 5 arrondissements de la grande ville de New-York? En sachant qu'il est incontournable d'aller se promener dans les grandes avenues et rues sans s'arrêter dans un stand BBQ et y manger les bons hot-dogs d'un sympathique New-yorkais. Il faut dire: '''Si Man BBQ''' (Staten Island, Manhattan, Brooklyn, Bronx, Queen). Bon appétit, bonne visite!. === Pays baltes === Vous confondez les pays baltes sur la carte ? C'est tout simple, ils sont placés par ordre alphabétique du nord au sud... [[w:Estonie|Estonie]], [[w:Lettonie|Lettonie]], [[w:Lituanie|Lituanie]] * Cela fonctionne aussi avec les appellations anglo-saxonnes : Estonia, Latvia, Lithuania et aussi avec les noms locaux : Eesti, Latvija, Lietuva. * Pour les capitales de ces pays : Estonie [[w:Tallinn|Tallinn]], Lettonie [[w:Riga|Riga]] et Lituanie [[w:Vilnius|Vilnius]] '''T'''rafic '''R'''outier '''V'''olumineux === Les tropiques === Les tropiques du Cancer et du Capricorne sont classés du nord au sud par ordre alphabétique. Le capricorne coule (en bas) car il a plus de lettres, il est plus lourd. L'antarctique aussi : l'arctique flotte. ou tropique du caNcer : N represente le Nord. ou Capricorne sonne comme "Cap Horn" donc au Sud === Les pays d'Amérique Centrale === *Du nord au sud : [[w:Bélize|Bélize]], [[w:Guatemala|Guatemala]], [[w:Honduras|Honduras]], [[w:Salvador|Salvador]], [[w:Nicaragua|Nicaragua]], [[w:Costa Rica|Costa Rica]], [[w:Panama|Panama]]. BGHSNCP soit: '''B'''eau '''G'''arçon '''H'''abitant '''S'''alvador '''N'''ettoie et '''C'''i'''r'''e les '''P'''lanchers ou Belle Guatemalaise Habitant Salvador, Nage sur la Côte du Panama. === Fleuves de Russie === D'ouest en est, les initiales des cinq principaux fleuves de [[w:Russie|Russie]] forment le mot "VOILA" : [[w:Volga|Volga]], [[w:Ob|Ob]], [[w:Ienisseï|Ienisseï]], [[w:Léna|Léna]], [[w:Amour (fleuve)|Amour]]. L'Ob, le Ienissei et la Léna sont les trois plus grands cours d'eau de Sibérie. === Pays du Moyen-Orient === Le Qatar est une presQu'île dans la péninsule arabiQue. Le Qatar peut être vu comme comme une Crête de CoQ juchée sur l'Arabie saoudite et s'ouvrant sur le golfe arabo-persiQue. === Pays d'Asie Centrale (en -stan) === * Du Nord au Sud et de l'Ouest à l'Est '''Kaz'''akhstan - '''Ou'''zbékistan - '''Ki'''rghizistan - '''Tu'''rkménistan - '''Ta'''djikistan - '''Af'''ghanistan - '''Pa'''kistan Kaz Ou Ki Tu Ta Af Pa Kazouki, tu taffes pas ? === Principales villes traversées par la Loire === * De l´Atlantique au Mont Gerbier de Jonc '''Na'''thalie '''an'''goisse '''to'''ujours les '''bl'''ondes '''or'''iginaires de '''Nevers''', elles '''ro'''ugissent '''sa'''ns '''pu'''deur. Nantes Angers Tours Blois Orléans Nevers Roanne Saint-Étienne Le Puy en Velay == Histoire == === Préhistoire === {{loupe|#Les périodes géologiques de l’ère primaire|# Les périodes géologiques de l’ère secondaire}} ==== Évolution des [[w:Homininae|homininés]] ==== Les '''Austral'''iens '''habil'''es eurent une '''érec'''tion, quand ils aperçurent, dans le '''néan'''t, des '''sapins''' gigantesques, <br /> ce qui donne, par ordre d'apparition<br /> [[w:Australopithèque|Australopithèque]], [[w:Homo habilis|Homo habilis]], [[w:Homo erectus|Homo erectus]], [[w:Homme de Néanderthal|Homme de Néanderthal]] et [[w:Homo Sapiens|Homo Sapiens]]. === Les 7 Merveilles du monde antique === "'''Mostapha''' ! '''J’attends''' la '''copie''' !" Variante: "'''Mostapha''' ! '''J’attends''' ta '''coloscopie''' !" ('''Mau'''solée d’Halicarnasse, '''Sta'''tue de Zeus à Olympie, '''Pha'''re d’Alexandrie, '''Ja'''rdins suspendus de Babylone, '''Tem'''ple d'Artémis à Éphèse, '''Co'''losse de Rhodes, '''Py'''ramides d’Égypte) === Les 7 rois de Rome === ''Ronutuann' tarsertar'' (qu'on retient mieux en imaginant le paresseux boucher Ronu : "Ronu, tuant tard, sert tard") ('''Ro'''mulus, '''Nu'''ma Pompilius, '''Tu'''llus Hostilius, '''An'''cus Martius, '''Tar'''quin l’Ancien, '''Ser'''vius Tullius, '''Tar'''quin le Superbe) === Les 11 [[w:Liste des empereurs romains|premiers empereurs romains]], dans l’ordre de leur règne === ''AuTiCaClauNéGalOViVesTiDo'' ('''Au'''guste, '''Ti'''bère, '''Ca'''ligula, '''Clau'''de, '''Né'''ron, '''Gal'''ba, '''O'''thon, '''Vi'''tellus, '''Ves'''pasien, '''Ti'''tus, '''Do'''mitien) Et les six suivants : ''NeTraHadAnMarCo'' (Nerva, Trajan, Hadrien, Antonin, Marc-Aurèle, Commode) Cesautica Clonegalo Vivestido CESar, AUguste, TIbere, CAligula CLAUde, NEron, GALba, Othon VItellus, VESpasien, TItus, Domitien === Les traités napoléoniens, dans l’ordre de leur signature === '''''CAV''''' (penser à une ''cave'') : ''Cambalu, Apresti, Viparis'' ('''Cam'''po Formio, '''Bâ'''le, '''Lu'''néville, '''A'''miens, '''Pres'''bourg, '''Ti'''lsit, '''Vi'''enne, '''Paris''') === Les présidents de la troisième République française === ''Thimagré Carcafauloufa Poindemidoudoule'' ('''Thi'''ers, '''Ma'''c-Mahon, '''Gré'''vy, '''Car'''not, '''Ca'''simir-Perier, '''Fau'''re, '''Lou'''bet, '''Fa'''llières, '''Poin'''caré, '''De'''schanel, '''Mi'''llerand, '''Dou'''mergue, '''Dou'''mer, '''Le'''brun) OU ce petit poème : Tire Mon Glaive Car Casse-Pierre Fort Loup Faillit Point Dèche Mille Dômes D'où Merle Brun === Les présidents de la cinquième République française === '''D'''es '''P'''illards '''G'''ouvernent, '''M'''ais '''C'''hacun '''S'''ubit '''H'''élas la Macronie '''D'''ur '''P'''armesan '''G'''orgonzola '''M'''ozzarella '''Ch'''auds '''S'''ervis. '''D'''ouce '''P'''atrie '''G'''auloise où '''M'''iaulent les '''Ch'''ats '''S'''iamois '''H'''eureux. <math>\Longrightarrow</math>De Gaulle, Pompidou, Giscard d'Estaing, Mitterrand, Chirac, Sarkozy, Hollande. '''D'''ouce '''P'''atrie '''G'''auloise où '''M'''iaulent les '''Ch'''ats '''S'''iamois '''H'''eureux et '''M'''alades. <math>\Longrightarrow</math>De Gaulle, Pompidou, Giscard d'Estaing, Mitterrand, Chirac, Sarkozy, Hollande, Macron. - Le sauveur de la France, Charles '''de Gaulle''', s’orthographie avec deux L. On peut retenir que de Gaulle a deux L, comme les deux barres de la croix de Lorraine, symbole de la France libre. On retient que la Gaule n’a qu’un L comme le L unique dans Celtes. Car "les Gaulois" est le nom que Jules César donne aux Celtes. === Les présidents américains à partir de Roosevelt === '''R'''udy '''T'''ente '''E'''n '''K'''araté '''J'''e '''N'''ique '''F'''abienne '''C'''omme '''R'''udy '''B'''ouche '''C'''ontre '''B'''ouche '''O'''utré '''T'''errifié. '''R'''oosevelt '''T'''rouva '''E'''léonore en '''K'''imono, '''J'''ames '''N'''e '''F'''ilma '''C'''arrément '''R'''ien, '''B'''rave '''C'''améraman '''B'''ouché et '''T'''êtu ! <math>\Longrightarrow</math>Roosevelt, Truman, Eisenhower, Kennedy, Johnson, Nixon, Ford, Carter, Reagan, Bush, Clinton, Bush, Obama, Trump. === Les dirigeants de l'URSS et de la Russie === '''L'''aissant '''S'''on '''K'''imono '''B'''leu '''À''' '''T'''rois '''G'''amins, '''E'''lle '''P'''ut '''M'''aintenir '''P'''outine. L = Lénine, S = Staline, K = Khrouchtchev B = Brejnev, À = Andropov, T = Tchernenko, G = Gorbatchev.....la "virgule" marque la chute du communisme, et E = Eltsine, P = Poutine, M = Medvedev, P = Poutine == Médecine == === Plan des muscles complexus === 1. Muscle semi-épineux de la tête * Sème tranquillement 156 graines dans un carré de terre pour 71 arbres épineux. (Le semi-épineux a pour origine les processus transverses de Th1 à TH5/Th6 et C4 à C7 et se termine sur les processus épineux de C7 ) th1) 2. Muscle longicissimus du cou *Louons tranquillement une sainte tu (la) sauteras. (Le muscle longicissimus du cou a pour origine les processus transverses de Th1 à Th5 et se termine sur les tubercules post de C3 à C7) === Les os du carpe === *''SSPP - TTCC'' (Initiales) * ''Sca-Lu-Py-Pi T-T-Go-Oc'' (Phonétique) <math>\Longrightarrow</math>('''Sca'''phoïde, (Semi-'''Lu'''naire)'''lu'''natum* , '''Py'''ramidal, '''Pi'''siforme - '''T'''rapèze, '''T'''rapézoïde, '''C'''apitatum ''', '''os '''C'''rochu ''')''' NB : dans la nouvelle nomenclature ce n'est plus le semi-lunaire mais le LUNATUM *On peut le voir sous un autre angle : ** PI - TRI - LU - SCA *: (pisciforme) (triquetrum) (lunatum) (scaphoide) ** HA - CA - TRI - TRA *: (hamatum) (capitatum) (trapézoide) (trapèze) Ou encore prendre les consonnes de ces 2 mots : *'''P'''é'''T'''a'''L'''e'''S''' : '''P'''isiforme - '''T'''riquetrum - '''L'''unatum - '''S'''caphoïde * a'''TT'''a'''CH'''e : '''T'''rapèze - '''T'''rapézoïde - '''C'''apitatum - '''H'''amatum *Trouvé par un étudiant : * ** '''S'''a'''L'''e '''T'''e'''P'''u, '''<nowiki>T'</nowiki>'''é'''T'''ais à '''CH'''ier. ** '''S'''uce '''l'''a '''t'''rique '''P'''atrick, '''t'''u '''t'''ireras '''Ch'''arlotte ** Le '''S'''carabée à '''L'''unettes '''T'''rie ses '''P'''ièces, '''T'''out '''T'''as est un '''C'''apital ('''h''')Amassé" ** '''S'''a'''L'''u'''T''' '''P'''ierre, '''T'''<nowiki/>'é'''T'''ais '''C'''haud '''H'''ier. === Les muscles épicondyliens médiaux (ex-épitrochléens) du membre supérieur === ''Grand Papa cuve et ronfle'' <math>\Longrightarrow</math>('''Grand pa'''lmaire, Petit '''pa'''lmaire, '''Cu'''bitus antérieur, '''Ron'''d pronateur, '''Flé'''chisseur commun superficiel) Une autre phrase est proposée "Grand Papa, Petit Papa, fléchit rondement le cul en avant" <u>Avec la nouvelle nomenclature</u> : ''Paulo Fuck Les Filles Sans Défense Faisant Un CAprice'' ''==> rond '''P'''ronateur, '''F'''léchisseur radial du carpe, '''L'''ong palmaire, '''F'''léchisseur '''S'''uperficiel des '''D'''oigts, '''Fl'''échisseur '''U'''lnaire du '''Ca'''rpe'' === Les muscles épicondyliens latéraux (ex-épicondyliens) du membre supérieur === <math>\Longrightarrow</math>'''2'''ème '''Ra'''dial, '''Ext'''enseur '''commun''', '''ext'''enseur '''propre''' '''du 5'''ème doigt, '''Court Su'''pinateur, '''Cu'''bital '''Post'''érieur, '''Anconé''' Deux rats excommuniés, expropriés du 5e ont cousu (court supinateur) le cul de la postière en cône. <u>Avec la nouvelle nomenclature</u> : ''Charlie Rêve d'Explorer Des Etoiles, 5 Etoiles Uniques Au Sanctuaire'' ''==>'' Court extenseur Radial du carpe, Extenseur commun des Doigts, Extenseur du 5ème doigt, Extenseur Ulnaire du carpe, Anconé, Supinateur === Les 12 paires de nerfs crâniens === ==== Ancienne nomenclature ==== *'''''O'''h '''O'''scar, '''m'''a '''p'''etite '''t'''héière '''m'''e '''f'''ait '''à''' '''g'''rand '''p'''eine '''s'''ix '''g'''rogs'' *'''''O'''h '''O'''scar, '''m'''a '''p'''etite '''t'''hérèse '''m'''e '''f'''ait '''à''' '''g'''rand '''p'''eine '''s'''ix '''g'''osses'' <math>\Longrightarrow</math>'''O'''lfactifs, '''O'''ptiques, '''M'''oteur oculaire commun, '''p'''athétiques, '''T'''rijumeau, '''M'''oteur oculaire externe, '''F'''aciaux, '''A'''uditifs, '''G'''losso-pharyngiens, '''P'''neumogastriques, '''S'''pinaux, '''G'''rand hypoglosse. * Nouvelle nomenclature *'''''OL'''ivia '''OPT'''<nowiki>e pour l'</nowiki>'''OC'''éan c'est '''TRO'''p '''TRI'''<nowiki>ste d'</nowiki>'''A'''ller '''FA'''ire des '''V'''isites '''G'''avantes quand les '''VAGUES''' '''A'''<nowiki>pportent l'</nowiki>'''HYP'''nose.'' *'''O'''h '''O'''h ! '''O'''scar ! '''T'''a '''T'''héière '''A''' '''F'''ait '''V'''ingt '''G'''rands '''V'''erres '''A''' '''H'''ector. *'' '''Ol'''ivier '''Op'''oil '''Ocul''' '''Troqu'''a '''Tri'''stement '''A'''vec '''Fa'''nny '''V'''ingt '''Gloss Par'''fums '''Va'''nille '''Accessoire'''<nowiki> d'</nowiki>'''Hy'''dratation.'' *'''''Ol'''é '''O'''<nowiki>scar d'</nowiki>'''Occ'''ident ! '''Tr'''availle ton '''Tri'''ceps, tes '''Abd'''o et tes '''F'''esses au '''WC'''. '''Gl'''isse '''vague'''ment ton '''accessoire''' et '''hip''' !'' *'''''O'''yez, '''O'''yez ! '''O'''bstinée '''T'''ortue '''T'''enace '''A''' '''F'''inalement '''V'''aincu, '''G'''rand '''V'''antard '''A''' '''H'''onte.'' *'''''O'''yé! '''O'''yé! '''O'''bstinée '''T'''ortue '''T'''enace '''A''' '''F'''inalement '''V'''aincu (la) '''G'''rande '''V'''ague '''À''' '''H'''awaii''. *'' '''Ol'''af '''Opt'''a '''Occ'''asionellement pour le '''Tro'''quet '''T'''andis qu' '''Abd'''el '''Fa'''isait '''V'''alser '''G'''rand'''-P'''ère '''Vague'''ment '''A'''utour de l' '''Hippo'''campe. '' *'''''O'''n '''O'''ccasion '''O'''livier '''T'''ries '''T'''o '''A'''nally '''F'''uck '''V'''arious '''G'''uys, '''V'''aginas '''A'''re '''H'''istory''. *'''''Ol'''a! '''Op'''hélie '''O cul Tro'''p '''Tri'''pant '''A Fa'''it '''Co'''quettement '''Glo'''usser '''Va'''lentin '''A''' l'<nowiki/>'''Hypo'''drome.'' <math>\Longrightarrow</math>'''O'''lfactif, '''O'''ptique, '''O'''cculomoteur, '''T'''rochléaire, '''T'''rijumeau, '''A'''bducens, '''F'''acial, '''V'''estibulo-Cochléaire ''ou'' '''C'''ochléo-vestibulaire, '''G'''losso-Pharyngien, '''V'''ague, '''A'''ccessoire, '''H'''ypoglosse. ==== Sensitif ou moteur ==== - Un dernier pour savoir la '''composante de chaque nerfs''' : Mots commençant par un '''S''' = sensitif, '''M''' = moteur, '''B''' = les deux (both). Ensuite les noms communs et les adjectifs sont parasympathiques (Money, brother, big, boobs). *'''''S'''ome '''S'''ay '''M'''oney '''M'''atters, '''B'''ut '''M'''y '''B'''rother '''S'''ays : '''B'''ig '''B'''oobs '''M'''atter '''M'''ost.'' ex: Boobs = 10e nerf (Vague), B = sensitif et moteur, nom commun = parasympathique. Ceci correspond aux caractéristiques du nerf vague ! - Une autre plus rigolote et moins décente : *'''''S'''eb '''S'''uces '''M'''oi '''M'''es '''D'''eux '''M'''amelons '''D'''e '''S'''ilicone '''D'''é-'''D'''é '''M'''e '''M'''anque'' '''S'''ahara '''S'''ablonneux (et) '''M'''er '''M'''orte, '''D'''eux '''M'''ondes '''D'''e '''S'''ilence (et) '''D'''éserts '''D'''e '''M'''ouvants '''M'''irages '''S''' = sensitif, '''M''' = moteur, '''D'''= les deux. Ainsi le 1{{er}} nerf crânien est sensitif. Le 9{{e}} et le 10{{e}} sont à la fois sensitifs et moteurs, etc. === Les 15 collatérales de l’artère maxillaire === '''''T'''on '''m'''épris '''p'''eut '''a'''mener '''m'''a '''t'''empête '''p'''etite '''b'''iche '''t'''ant aimée. '''Un''' '''p'''etit '''c'''âlin '''p'''eut '''p'''ardonner'' <math>\Longrightarrow</math>('''T'''ympanique, '''M'''éningée moyenne, '''P'''etite méningée, '''A'''lvéolaire inférieure, '''M'''asseterine, '''T'''emporale profonde postérieure, '''P'''térygoïdienne, '''B'''uccale, '''T'''emporale profonde antérieure, '''A'''lvéolaire supérieure, '''In'''fra-orbitaire, '''P'''alatine descendante, du '''C'''anal ptérygoïdien, '''P'''térygo-palatine, '''P'''haryngienne) (N.B. Non! l'artère pharyngienne est une branche de la carotide externe!) les 15 branches dans l'ordre: un '''T'''ic '''MENING'''é '''PE'''ut '''DE'''venir '''MA'''léfique, '''T'''andis qu'un '''BU'''bon '''TE'''rriblement '''AL'''gique '''P'''eu'''T''' être '''SOU'''lagé '''VI'''te '''PA'''r une '''PT'''yaline '''SP'''écifique '''( T'''ympanique, '''MENINGE'''é moyenne, '''PE'''tite méningée, '''DE'''ntaire inférieure, '''MA'''ssétérine, '''T'''emporale profonde moyenne, '''BU'''ccale, '''TE'''mporale profonde antérieure, '''AL'''véolaire, '''PT'''érygoïdienne, '''SOU'''s orbitaire, '''VI'''dienne, '''PA'''latine descendante, '''PT'''érygopalatine, '''SP'''hénopalatine.) === Les branches de l'artère axillaire === '''TH'''éodore '''a''' '''m'''angé '''s'''on '''c'''a'''c'''a <math>\Longrightarrow</math>'''TH'''oracique supérieure, '''A'''cromiothoracique, '''M'''ammaire Interne, '''S'''capulaire et les deux '''C'''irconflexes Cette artère se trouve dans la région de l'aisselle et pas ailleurs. === Branche de l'artère carotide externe : === '''''T'''ous '''L'''es '''F'''rançais '''O'''nt '''A'''pplaudi le '''P'''résident '''M'''onsieur '''T'''hiers''. <math>\Longrightarrow</math>('''T'''hyroïdienne supérieure, '''L'''inguale, '''F'''aciale, '''O'''ccipitale, '''A'''uriculaire postérieure, '''P'''haryngienne ascendante, '''M'''axillaire interne, '''T'''emporale superficielle): === Collatérales de la carotide externe === '''''T'''ire '''l'''a '''f'''icelle, '''p'''ortier ! '''O'''uvre '''à''' '''t'''on '''m'''aître '''r'''apidement'' <math>\Longrightarrow</math>('''T'''hyroïdienne supérieure, '''L'''inguale, '''F'''aciale, '''P'''haryngienne ascendante, '''O'''ccipitale, '''A'''uriculaire postérieure, '''T'''emporale superficielle, '''M'''axillaire, '''R'''ameau parotidien) '''T'''ou'''s''' '''l'''es '''F'''rançais '''a'''cclament '''O'''bama '''p'''résident <math>\Longrightarrow</math>('''T'''hyroïdienne '''s'''upérieure, '''L'''inguale, '''F'''aciale, '''A'''uriculaire postérieure, '''O'''ccipitale, '''P'''haryngienne ascendante) '''S'''ome '''a'''ngry '''l'''ady '''f'''igured '''o'''ut '''p'''ost '''m'''enopausal '''s'''yndrom. <math>\Longrightarrow</math>('''S'''uperior thyroidal, '''A'''scending pharyngeal, '''L'''ingual, '''F'''acial, '''O'''ccipital, '''P'''osterior auricular, '''M'''axillary, '''S'''uperficial temporal) Ce dernier, bien qu'en anglais, a l'avantage d'indiquer les artères dans l'ordre ascendant. '''T'''u '''P'''eux '''L'''a '''F'''ourrer '''O'''u l'''A''' '''M'''anger '''T'''oute. <math>\Longrightarrow</math>('''T'''hyroïdienne supérieure, '''P'''haryngée ascendante, '''L'''inguale, '''F'''aciale, '''O'''ccipitale, '''A'''uriculaire postérieure, '''M'''axillaire, '''T'''emporale superficielle) Ce dernier est une variante québécoise à caractère sexuel qui a également l'avantage d'indiquer les artères dans l'ordre ascendant. === Les collatérales de l’artère ophtalmique === '''''R'''emets '''l'''es '''c'''apotes '''s'''ans '''n'''ous '''f'''aire '''p'''<nowiki>erdre l'</nowiki>'''é'''rection'' <math>\Longrightarrow</math>(Centrale de la '''R'''étine, '''L'''acrymales, '''C'''iliaires, '''S'''upra-orbitaire, '''N'''asales, '''F'''rontales, '''P'''alpébrales, '''E'''htmoïdales antérieures et postérieures) === Les rameaux du plexus lombaire === '''Il''' '''hyp'''notise '''il'''lico l''''igu'''ane '''gé'''ant et '''fé'''roce et lui '''c'''oupe '''l'''ittéralement '''la''' '''cuisse''' qui '''fai'''sait '''ob'''struction '''car''' elle '''l'omb'''rageait. <math>\Longrightarrow</math>('''il'''io-'''hyp'''ogastrique; '''il'''io-'''ingu'''inal; '''gé'''nito-'''fé'''moral; '''c'''utané '''l'''atéral de '''la''' '''cuisse'''; '''fé'''moral; '''ob'''turateur; du muscle '''car'''ré des '''lombes''') === Les rameaux du plexus sacré === '''Si''' un '''gl'''acier '''sup'''er '''inf'''idèle est '''honteux''' d’avoir '''per'''foré le '''rect'''um et '''élev'''é '''l’anus''' d’un '''cu'''isinier '''po'''urtant '''c'''onsentant, un '''curé''' '''fe'''ra un '''ju'''gement '''im'''partial en '''oub'''liant l’'''in'''acceptable. <math>\Longrightarrow</math>('''sci'''atique; '''gl'''utéal '''sup'''érieur et '''inf'''érieur; '''honteux'''; '''pir'''iforme; '''rect'''al supérieur ; '''élév'''ateur de '''l’anus'''; '''cu'''tané '''po'''stérieur de la '''c'''uisse; '''carré''' '''fé'''moral et '''ju'''meau '''in'''férieur; '''ob'''turateur '''in'''terne) === Les muscles du grand trochanter === Mon '''g'''ars, '''troqu'''ons: Le '''Petit''' '''Pierre''' et '''les jumeaux''' '''moyen'''nant '''fesses''' à '''obtur'''er. <math>\Longrightarrow</math>('''G'''rand '''Troch'''anter: '''petit''' fessier; '''pir'''iforme; '''jumeaux''' supérieur et inférieur; '''moyen fess'''ier; '''obtur'''ateurs internes et externes) === Les muscles de la patte d'oie === *SA GRA - Tte ("ça gratte") *# Sartorius (ex-Couturier) *# Gracile *# Tendineux **CGT **Sartre est grat et tendre **ça gratte === Les ménisques du genou === '''''CI'''TR'''OE'''N'' (Le ménisque en forme de '''C''' est le ménisque '''I'''nterne ; le ménisque en forme de '''O''' est le ménisque '''E'''xterne) === Les obturateurs pelvi-trochantériens === Être '''ex-empt''' d''''im-pôt'''s (L'obturateur '''ex'''terne s'insère à la face '''ant'''érieure de l'os coxal, l'obturateur '''in'''terne s'insère à la face '''po'''stérieure de l'os coxal) == Mythologie == === Les 3 Grâces === '''Aglaé''' offre une u'''sine''' à Na'''thalie''' (variante : Aglaé offre Rosine à Nathalie) ** Aglaé, Euphrosine et Thalie === Les 9 Muses === Voici l'astuce des étudiants en grec à l'école pour retenir les neuf muses : '''''Cl'''ame, '''Eu'''gène, '''ta''' '''mél'''odie, '''terr'''ible '''air''' '''pol'''onais, o'''ura'''gan '''cal'''culé'' <math>\Longrightarrow</math>('''Cl'''io, '''Eu'''terpe, '''Tha'''lie, '''Mel'''pomène, '''Ter'''psichore, '''Ér'''ato, '''Pol'''ymnie, '''Ura'''nie, '''Cal'''liope) Calliope porte une couronne d’or, Clio une couronne de laurier, Érato une couronne de myrtes et de roses, Euterpe une couronne de fleurs, Melpomène une couronne de pampre de vigne, Polymnie une couronne de perles, Terpsichore une couronne de guirlandes, Thalie une couronne de lierre, et Uranie une couronne d'étoiles === Les dieux grecs === ''Hazah Phadhadah'' <math>\Longrightarrow</math>('''H'''éra, '''A'''phrodite, '''Z'''eus, '''A'''pollon, '''H'''éphaïstos, '''P'''oséidon, '''H'''ermès, '''A'''rtémis, '''D'''ionysos, '''H'''adès, '''A'''théna, '''D'''éméter, '''A'''rès, '''H'''estia) Notons que la plupart des dieux grecs commencent par la lettre "A" ou "H". {| class="wikitable" |- | Hestia || arrêta || d'aimer || Zeus<small>:</small> || possédée || <small>par</small> Dionysos<small>,</small> || <small>elle</small> erra || <small>dans</small> Athènes<small>,</small> || affolée<small>,</small> || <small>et fit</small> l'apologie || <small>de l'</small>art || effarant || d'Hermes || et d'Hadès |- | Hestia || Arès || Demeter || Zeus || Poséidon || Dionysos || Héra || Athéna || Aphrodite || Apollon || Artémis || Héphaïstos || Hermes || Hades |} === Les dieux romains === '''''J'''eune '''v'''euve '''j'''oyeuse '''c'''herche '''v'''ieux '''b'''aron '''m'''ême '''m'''alade '''a'''fin '''d'''e '''v'''ivre '''m'''ieux'' '''p'''oint <math>\Longrightarrow</math>('''J'''unon, '''V'''énus, '''J'''upiter, '''C'''érès, '''V'''ulcain, '''B'''acchus, '''M'''ercure, '''M'''inerve, '''A'''pollon, '''D'''iane, '''V'''esta, '''M'''ars, '''P'''luton) == Religion == === Les dimanches avant Pâques === Les petits protestants alsaciens (et allemands aussi, sans doute) apprenaient jadis le nom des dimanches qui précédaient Pâques grâce à la phrase : « '''I'''n '''R'''echter '''O'''rdnung '''L'''ehre '''J'''esu '''P'''assion », ce qui signifie : « Apprends dans le bon ordre la passion de Jésus ». On peut aussi dire : « '''I'''n '''R'''ektors '''O'''fen '''L'''iegen '''J'''unge '''P'''almen », ce qui veut dire : « Dans le poêle du recteur se trouvent de jeunes palmes (allusion à rameaux) ». Et ils retrouvaient : '''I'''nvocavit, '''R'''eminiscere, '''O'''culi, '''L'''aetare, '''J'''udica et '''P'''almarum (Palmsonntag - dimanche des Rameaux). === Péchés capitaux === Les initiales des [[w:sept péchés capitaux|sept péchés capitaux]] sont rassemblés dans le mot « Pô glacé ». ('''p'''aresse, '''o'''rgueil, '''g'''ourmandise, '''l'''uxure, '''a'''varice, '''c'''olère et '''e'''nvie) ** Autre expression pour retenir les 7 péchés capitaux : " CE GALOP ". (Colère, Envie, Gourmandise, Avarice, Luxure, Orgueil, Paresse) ** L' '''ENV''' ie '''EN V'''eut, lorsqu'on lui demande de travailler, la '''P'''aresse se '''P'''ousse, l' '''O'''rgueil se pense plus haut ('''O''') que les autres, la '''G'''ourmandise mange du '''G'''ateau et du '''G'''ras, la '''L'''uxure va dans un hot-'''L''', l' '''A'''varice en '''A''', la '''C'''olère '''C'''rie. **Ou encore une phrase: '''L'''es '''G'''rands '''E'''sprits '''O'''bligent '''C'''ertainement '''A''' '''P'''enser (luxure, gourmandise, envie, orgueil, colère, avarice, paresse) (entendu cité par Jacques Lacarrière lors d'une interview sur France-Culture)' ' **Une phrase courte et très facile à mémoriser, car elle ne fait pas appel qu'aux initiales qui demanderaient encore beaucoup d'efforts pour les identifier. Ici, l'utilisation de mots qui agissent immédiatement sur la divulgation des 7 péchés: ''''<nowiki>Par goût, Colette envie l'orgue luxueux d'Avarice'</nowiki>''' ('''Par'''esse, '''gou'''rmandise, '''colè'''re, '''envie''', '''orgue'''il, '''lux'''ure, '''avarice''') === Épîtres de Paul === Rococo Galéphicol ThèThè TimTim TiPhilHé ('''Ro'''mains, 1 '''Co'''rinthiens, 2 '''Co'''rinthiens, '''Gal'''ates, '''Éph'''esiens, '''Phi'''lippiens, '''Col'''ossiens, 1 '''The'''ssaloniciens, 2 '''The'''ssaloniciens, 1 '''Tim'''othée, 2 '''Tim'''othée, '''Ti'''te, '''Phil'''émon, '''Hé'''breux) === Apôtres de Jésus === '''S'''ouvent '''a'''bsent, '''J'''ean '''J'''aures '''p'''erdait '''b'''êtement '''t'''oute '''m'''onnaie le '''j'''our '''J''' de '''s'''on '''j'''eûne. '''S'''imon, '''A'''ndré, '''J'''acques, '''J'''ean, '''P'''hilippe, '''B'''arthélémy, '''T'''homas, '''M'''atthieu, '''J'''acques, '''J'''ude, '''S'''imon, '''J'''udas. '''Si JaJa Ma embarté Juju, J'enfile To Pierre''' '''Si'''mon, '''Ja'''cques, '''Ja'''cques, '''Ma'''tthieu, '''An'''dré, '''Barthé'''lémy, '''Ju'''de, '''Ju'''das, '''Jean''', '''Phil'''ippe, '''Tho'''mas, '''Pierre'''. === '''Vertus Cardinales''' === '''P'''rudence, '''J'''ustice, '''F'''orce et '''T'''empérance se retient avec : " '''P'''our '''J'''ésus, '''F'''ais '''T'''out ! ", les premières lettres des mots rappellent les quatre vertus cardinales. === Les sept sacrements catholiques === ''BECOROM'' : # Baptême # Eucharistie # Confirmation # Onction des malades # Réconciliation # Ordination # Mariage === Les sept dons de l'Esprit Saint === ''P C DS FCC (paie ces déesses fichier central des chèques)'' # Sagesse # Discernement # Conseil # Force # Connaissance # Crainte du Seigneur # Piété == Navigation == === Marine === ==== Bâbord/Tribord ==== '''''Bâ'''bord c’est g'''a'''uche, t'''r'''ibord, c’est d'''r'''oite.'' (si on regarde vers la partie avant du bateau) On peut aussi le retenir avec le mot batterie (que l’on prononce généralement « BaTri »), on a Ba à gauche (comme bâbord) et Tri à droite (comme tribord) OU plus simple avec "BaTeau". {| class="wikitable" | Bâ | Tri |- | Bâbord | Tribord |- | Gauche | Droite |} Variante : la seconde lettre de b'''â'''bord est la même que la seconde lettre de g'''a'''uche et la seconde lettre de t'''r'''ibord est la même que la seconde lettre de d'''r'''oite. Variante : Babo<s>rd ga</s>uche = Babouche. On ne retrouve qu'un A dans bAbord et gAuche et un I dans trIbord et droIte. ==== Couleur et signalisation bâbord et tribord ==== Couleur des feux de navigation d'un bateau: {| | Bâbord | '''R'''ouge |- | Tribord | '''V'''ert |} Lu de gauche à droite cela fait '''RV''' ou le prénom '''Hervé'''. ''Un marin emporte toujours Un Tricot Vert et Deux Bas Si Rouges'' = chiffres '''impairs''', '''Tri'''bord, '''Cô'''ne, '''Vert''', et chiffres '''pairs''', '''Bâ'''bord, '''Cy'''lindre, '''Rouge'''. Ou encore : en entrant au port, on trouve les balises COniques VERTES à TRIbord, et les balise CYlindrique ROUGES à BAbord. En [[w:aviron|aviron]], '''T.G.V''' à savoir '''T'''ribord-'''G'''auche-'''Vert''' car le rameur est dos à l'embarcation et donc tribord se situe à sa gauche. ==== Compartimentage ==== Le compartimentage est la méthode employée pour assurer la sécurité d'un navire contre les voies d'eau. Il consiste à diviser l'espace en compartiments étanches en-dessous de la ligne de flottaison. Il résulte de ces nombreux cloisonnements une difficulté à localiser, notamment sur les gros bâtiments, tel ou tel lieu précis (cabine, soute, etc.). Afin de régler cette difficulté se pratique un référencement des locaux selon une numérotation en quatres chiffres/lettres : la tranche (de la proue vers la poupe), le pont (de la surface vers le fond à partir du pont principal 0 situé immédiatement au-dessus de la ligne de flottaison), le rang (sous-section de tranche) et le bord (local dans un rang donné). Ex. : A216 signifie que le local se trouve dans la tranche alpha, au niveau du deuxième pont inférieur (ou faux pont), au premier rang, premier local à bâbord en partant de la ligne médiane (ou de la coursive centrale) du navire. La mémorisation de l'ordre des bords s'effctue avec la phrase mnémotechnique suivante : "j'aime les femmes en slip", le nombre de lettres de ces mots donnant la suite 1-5-3 (impairs sur tribord) 6-2-4 (pairs sur bâbord). ==== Nœuds ==== La phrase « ''Le serpent sort du trou, tourne autour de l'arbre, et rentre dans le trou'' » permet de se rappeler de la méthode pour faire un [[w:nœud de chaise|nœud de chaise]]. === Aviation === Chez les pilotes d’avions utilisant le système [[w:Precision_Approach_Path_Indicator|Vasi]] pour trouver l’altitude correcte à l’atterrissage en [[w:vol à vue|vol à vue]], des panneaux rouges et blancs à côté de la piste leur fournissent de précieuses indications qui ont abouti à cette comptine : :« ''White over white, you're high as a kite'' :''Red over white, you're right'' :''Red over red, you're dead'' » (Blanc sur blanc : trop haut, rouge sur blanc : correct, rouge sur rouge : trop bas). Pour récolter toutes les informations nécessaires pour compléter leur feuille de route, les pilotes aguerris utilisent la phrase : '''''R'''''etranchez '''v'''otre '''d'''érive, '''c'''ela '''v'''ous '''d'''onne '''c'''haque '''m'''esure '''d'''u '''c'''ap '''c'''ompas. Route vraie - X (dérive) / Cap vrai - Déclinaison / Cap magnétique - déviation / Cap compas Pour le décollage : CC PP VV TT (ou 3T) Compas, Conservateur de cap (ou gyro compas), Phares, Pompe, Volets, Verrière, [[w:Transpondeur|Transpondeur]], Top, (Talons au sol). Cela dépend bien sûr du type d'avion. Pour l’observateur au sol et face à l’avion, contrôleur, mécanicien de piste, le rouge est à droite et le vert est à gauche. Très pratique la nuit, on ne voit rien d’autre que les feux. Moyen mnémotechnique : Quand on se sert du vin à boire, on a le "rouge" dans la main droite et le "verre" dans la main gauche. Ou plus facile à retenir : Comme en politique, le rouge est toujours à gauche ! == Vie quotidienne == La vie quotidienne regorge de moyens mnémotechniques plus ou moins utiles. === Droite et Gauche === Se souvenir de sa main habile (droitier ou gaucher) Utiliser les initiales en majuscules (G et D) : l'arrondi est du côté correspondant (à Gauche pour G), (à droite pour D). En anglais, pour Left et Right (Gauche et Droite). On peut faire un L avec la main gauche, avec le pouce et l'index et non avec la main droite. D'où Left=gauche et right=droite. === Mois courts et mois longs === En mettant ses poings fermés côte à côte, les bosses des [[w:phalange|phalange]]s peuvent correspondre aux mois de 31 jours du [[w:calendrier|calendrier]] et les creux entre chacune aux mois de 30 jours ou moins. Et sans compter la jonction entre les mains comme un creux. On peut aussi le faire avec une seule main (c'est mieux pour les enfants, car ça permet de suivre le décompte des mois avec un doigt de l'autre main...) : on commence sur le premier sommet pour janvier, on s'arrête sur le dernier sommet pour juillet et, pour les mois suivants, on repart en arrière en comptant de nouveau le sommet (ou bien, on recommence au sommet initial, pour bien marquer les 2 mois de 31 jours). Attention ! Juillet-Aout ont 31 Jours mais Décembre-Janvier aussi !! === Heure d’été, heure d’hiver === En été on avance d’une heure, car "'''é'''té" et "'''a'''vance" commencent par une voyelle. En été le soleil qui passe à travers les volets nous réveille tôt car on dort une heure en moins. En hiver on recule d’une heure, car "'''h'''iver" et "'''r'''ecule" commencent par une consonne. En hiver on hiberne, donc on dort une heure en plus. Le changement d’heure se faisant en avril (!) et en octobre : * OCTOBRE finit par RE donc on REcule * ''AVRIL commence par AV donc on AVance'' Hélas..... cette information est affichée sur de très nombreux sites.. alors que le changement annoncé en AVRIL.. est en réalité réalisé le dernier dimanche de MARS ! Comme le changement a lieu en MARS et OCTOBRE, et qu'en général on ne peut pas (ou ne doit pas) faire reculer une aiguille sur une montre analogique : * MARS étant plus court (4 lettres), on avance de 1 heure, * OCTOBRE étant plus long (7 lettres), on avance de 23 heures (ou 11 heures pour les montres et horloges analogiques sans date). Pour les anglophones : Spring Forward, Fall Back (Spring étant le Printemps et Fall l'Automne)... === Retrouver de tête le nom du jour de la semaine quelle que soit la date donnée === {{pas clair}} Par convention, on associe les chiffres aux lettres suivantes : <pre> 0 = S, Z 1 = T, D 2 = N, Gn 3 = M 4 = R 5 = L, Y, ill 6 = CH, J, Ge 7 = K, Qu, Gu 8 = F, Ph, V 9 = P, B </pre> ==== N°1 - liens entre jours et lettres : ==== Lundi est le premier jour de la semaine . Donc '''Lundi = 1''' mardi = 2 ... ==== N°2 - liens entre mois et lettres ==== ===== a) correspondances pour année normale ===== : ** Janvier Février Mars... deviennent "'''S'''a'''M''' '''M'''e '''J'''e'''T'''e'''R'''a a'''G'''e'''N'''ou'''iLL'''é '''S'''on '''M'''a'''iLL'''ot" '''S''' = Janvier ; '''M''' = Février ; '''M''' = Mars '''J''' = Avril ; '''T''' = Mai ; '''R''' = Juin '''g'''= Juillet ; '''n'''= Août ; '''L'''= Septembre '''s'''= Octobre ; '''m'''= Novembre ; '''L'''= Décembre ===== b) correspondances pour année bissextile ===== "'''G'''i'''N'''o '''M'''e '''J'''e'''T'''e'''R'''a ..." '''G''' = Janvier ; '''N''' = Février ; etc ===== N°3 - facteur en fonction du siècle ===== ====== a) Avant le 4 octobre 1582, enlever 7 au siècle (''' -7''' ) ====== Année 670 : siècle 6 '''-7''' = 0 Année 1100 : siècle 11 '''-7''' = 4 Et le résultat on le transforme : 4 devient 0 , 3 devient 1 , 5 devient 6 , 2 reste 2 et inversement. Moyen mnémotechnique (suivant la convention) : RuSé MaTou :: 4-0 3-1 NoNNe LouChe :: 2-2 5-6 ====== b) À compter du 4 Octobre 1582 ====== soit le '''4''' - '''10''' - '''1582''' , c'est à dire le jour où le '''R'''oi '''T'''hé'''S'''ée '''T'''é'''L'''é'''PH'''o'''N'''a , on enlève 4 et non 7 , autant de multiple possible : **Année 1800 : siècle 18 - ( '''4'''x4 ) = 2 **Année 2000 : siècle 20 - ( '''4'''x5 ) = 0 La transformation donne 0-6 1-4 2-2 3-0 {| class="wikitable" |- | 0 || 1 || 2 || 3 |- | '''6''' || '''4''' || '''2'''|| '''0''' |- | '''CH'''è... || ...'''r'''e || '''N'''iai... || '''se''' |- | 0-6 || 4-1 || 2-2 || 0-3 |} ==== Calcul pour le 26 septembre 1955 ==== Il y a plusieurs opérations à réaliser en se conformant aux règles citées . **26 Sept 1955 règle 2a :: septembre = L = ''5'' **26+''5'' = 31 règle 3b ** 31-(7x4) = '''''3''''' **55 - (7x4) = 55-28= 27 **27 + (27/4) = 27+6 = 33 **33 - (7x4 ) = 33-28= '''''5''''' ** '''''5 + 3 ''''' =''''' 8''''' comme il y a 7 jours dans la semaine : *''''' 8-7 '''''= '''1''' le 1 correspond à '''lundi''' donc le 26 septembre 1955 était un '''lundi''', toute la journée !!! ==== Sinon pour l'année cours, passée ou à venir, ==== le moyen le plus simple et le plus rapide est de diviser l'année en trimestres . Pour chaque mois, on cherche le quantième du premier dimanche par exemple . On fait une phrase par trimestre et pour trouver la correspondance on rajoute 7 + de 1 à 6 . Exemple, pour 2012, {| class="wikitable" |- ! Janvier !! Février !! Mars |- | '''1''' || '''5''' || '''4''' |- | '''D'''e || '''<nowiki>L'</nowiki>''' || ai'''R''' |} Donc, le premier dimanche de Janvier 2012 est le 1er janvier . Pour aller à mon Rendez vous du 3 je rajoute 2. ** Selon ma convention déjà vue , lundi=1 mardi=2 mercredi=3 jeudi=4 vendredi=5 samedi=6 et dimanche=7 . Alors 2 correspond à mardi donc le 3 Janvier 2012 est mardi pour aller au 18 janvier 2012 , une semaine ayant 7 jours, même sous le règne actuel, je retire autant de semaine complète que possible . Donc 18 - (2x7 ) = 18 - 14 = 4 . Surprise, le 18 janvier 2012 sera un mercredi . Soit parce que j'ai rajouté 3 jours au dimanche, soit parce que j'ai décidé une fois pour toutes que le dimanche est le premier jour de la semaine, donc le mardi le 2ème etc ... ** dans ce cas, dimanche=1 mardi=2 mercredi=4 jeudi=5 vendredi=6 samedi=7 dimanche=8-7=1 C'est selon sa préférence intellectuelle . Je vous laisse continuer pour février et les autres trimestres. {| class="wikitable" |- ! janvier !! février !! mars !! avril !! mai !! juin !! juillet !! aout !! sept !! oct !! nov !! déc |- | 1 || 5 || 4 || 1 || 6 || 3 || 1 || 5 || 2 || 7 || 4 || 2 |- | D || l || r || t || ch || m || d || l || n || k || r || g |- | de || l' || air || tu || chô || me || dans || la || nuit || qui || rè || gne |} === Valeur d'un Euro en [[w:Franc français|Francs français]] === Selon le même principe que pour les décimales de ''π'' : {| border="0" cellpadding="0" cellspacing="1" |align="center"|''Chacun'' |&nbsp; |align="center"|''saura'' |&nbsp; |align="center"|''enfin'' |&nbsp; |align="center"|''convertir'' |&nbsp; |align="center"|''notre'' |&nbsp; |align="center"|''monnaie'' |- !align="center"|6 !align="center"|, !align="center"|5 | !align="center"|5 | !align="center"|9 | !align="center"|5 | !align="center"|7 |} === Morse === Le code morse est facilement mémorisable à l’aide des codes courts et longs remplacés par des syllabes. Le code long (-) remplacé par une syllabe en "O". Le code court (.) remplacé par une des autres voyelles. Ex : A = .- = Au/tO (une syllabe en A pour le . et une syllabe en O pour le -) La liste complète est [[w:Alphabet Morse#Tableau Mn.C3.A9motechnique|Ici]] === Les vins === ==== AOC de la côte de Nuits ==== Un mnémonique permettant de se rappeler des [[wikipedia:AOC|AOC]] communales de la [[wikipedia:Côte de Nuits|Côte de Nuits]], et dans l'ordre géographique en plus, par Paul Brunet, auteur du livre ''Le vin et les vins au restaurant'' : ''Messieurs, faites gaffe, mon chat vous voit noir'' (Marsannay, Fixin, Gevrey-Chambertin, Morey-Saint-Denis, Chambolle-Musigny, Vougeot, Vosne-Romanée, Nuits-Saint-Georges). ==== Nom des bouteilles de vin ==== Ce moyen mnémotechnique permet de mémoriser les principales tailles de [[w:bouteille de vin|bouteilles]] dans l'ordre croissant de contenance : * Car de bon matin je remarquais mal sa banalité naturelle (quart, demi, bouteille, magnum, jéroboam, réhoboam, mathusalem, salmanazar, balthazar, nabuchodonosor). Autre phrase mnémo, plus complète : * PICARD FIT : DE BON MATIN, JE REMARQUE SA BANALITÉ, SA MATIERE SI PAUVRE. Pour se rappeler tous les contenants de vin ou de champagne : * PIccolo, QUARt, FIllette, DEmi-bouteille, Bouteille, MAgnum, Jeroboam, REhoboam, Mathusalem, SAlmanazar, Balthazar, Nabuchodonosor, SAlomon, Melchisédech, Souverain, Primat. === <u>Dresser une table</u> === Pour se souvenir d'où mettre la fourchette et le couteau : Four'''<u>ch</u>'''ette à gau'''<u>ch</u>'''e, couteau à droite. ou encore : A, B, '''C''', '''D''' ... '''C'''outeau à '''D'''roite. E, '''F''', '''G''', H ... '''F'''ourchette à '''G'''auche == Cinéma, Bande dessinée... == === Dupond et Dupont === Pour différencier les deux [[w:Dupond et Dupont|dupondt]] de la bande dessinée [[w:Les Aventures de Tintin et Milou|Les Aventures de Tintin et Milou]], celui à la moustache tombante comme un D est Dupond, celui à la moustache pointue comme les barres du T est Dupont. === Les 7 nains === '''A''' '''J'''ouer '''P'''resque '''S'''eul '''T'''u '''D'''eviens '''G'''rincheux * Atchoum, Joyeux, Prof, Simplet, Timide, Dormeur, Grincheux : les 7 nains dans Blanche Neige... === Les Simpson === Pour différencier les deux sœurs de Marge, on observe les cheveux. Patty n'a pas de raie au centre et Selma a les cheveux découpés en deux blocs par une raie ou on regarde les boucles d'oreilles qui sont différentes Sinon, regarder les boucles d'oreilles de Patty, elles sont triangulaires (P comme Pythagore). == Sport == === Escalade === ''Pour la prochaine longueur je reste en bas, donc je me vache au plus bas.'' <br> :En escalade, permet de savoir sur quel point d'assurage se vacher lors du relais '''réversible''' uniquement. == Voir aussi == === Article connexe === * [[w:Code chiffres-sons|Code chiffres-sons]] === Liens externes === * [https://jeretiens.net -JeRetiens.net- Libre recueil ayant pour objectif de rassembler tous les trucs et astuces mnémotechniques pour retenir et apprendre plus facilement.] * [http://www.finallyover.com/categorie-1079667.html Méthodes thématiques de mémorisation] * [http://www.echolalie.org/wiki/index.php?ListeMnemotechnique Liste mnémotechnique] * [http://trucsmaths.free.fr/Pi.htm#poeme Le nombre pi] * [http://www.francaisfacile.com/exercices/exercice-francais-2/exercice-francais-75628.php francaisfacile.com] === Références === <references /> [[Catégorie:minilivres]] t5e6v43qillw061b47qwbuvy2vqe78a Wikilivres:Tous les livres 4 14152 772670 772662 2026-09-21T14:17:34Z Xhungab 23827 772670 wikitext text/x-wiki <big>Cette page présente [[:Catégorie:Livres par titre|tous les livres]] disponibles sur Wikilivres, y compris les [[:Catégorie:Ébauches sans ressources suggérées|ébauches]], les [[:Catégorie:Livres en cours de rédaction|livres en cours de rédaction]], et les [[:Catégorie:Feuilles volantes|feuilles volantes]].</big> N'hésitez pas à poursuivre l'écriture ou à améliorer les livres ou les pages en cours de création. Un Wiki est fait pour cela. Si vous le désirez, vous pouvez aussi contacter les personnes qui ont déjà contribué sur les pages que vous voulez modifier. Cela peut se faire en cliquant sur l'onglet « Voir l'historique » des pages en question. [[Fichier:Pedia-shelf-1.jpg|right|250px]] __NOEDITSECTION__ La bibliothèque contient [[Spécial:Statistics|{{NUMBEROFARTICLES}}]] pages réparties en [[:Catégorie:Livres par titre|{{formatnum:{{NUMBEROFBOOKS}}}} livres]], dont [[:Catégorie:Livres avec version PDF|{{PAGESINCATEGORY:Livres avec version PDF}}]] téléchargeables en PDF. {{message|clear=left|style=margin:0.2em 0;|Il existe, pour le moment, deux systèmes d'indexation internes pour trouver du contenu : * Le [[#Tous les livres par catégorie|système de catégories]] {{100}}. La quasi-intégralité des livres peuvent être trouvés via ce système. * La [[#Tous les livres selon la classification décimale universelle|classification décimale universelle]] {{25}}, utilisée dans les bibliothèques. Hélas, beaucoup de livres ne sont pas encore rangés selon cette classification, préférez pour le moment la première méthode ou la [[Wikilivres:Rechercher un livre|recherche de livre]]. }} == Tous les livres par catégorie == <div class="flex-content"> <div class="flex-content-half"> <div class="headerbleu">Livres par thème</div> <categorytree hideroot="on" mode="pages" style="border:1px solid #A0A0A0; padding:0.7ex; padding-right:1em;">Livres par thème</categorytree> </div> <div class="flex-content-half"> <div class="headerbleu">Livres par niveau d'avancement</div> <categorytree hideroot="on" mode="pages" style="border:1px solid #A0A0A0; padding:0.7ex;">Livres par niveau d'avancement</categorytree> </div> </div> == Tous les livres selon la classification décimale universelle == {{:Wikilivres:CDU}} [[Catégorie:Wikilivres]] 2xm9n2uojntd3wx2ogqxf7aisj5ok2v Fonctionnement d'un ordinateur/Les composants d'un processeur 0 65784 772676 765351 2026-09-21T14:30:43Z Mewtow 31375 /* L'unité de chargement */ 772676 wikitext text/x-wiki Dans le chapitre sur le langage machine, on a vu le processeur comme une espèce de boite noire contenant des registres qui exécutait des instructions les unes après les autres. Mais on n'a pas encore vu ce qu'il y a dans la boite noire. Pour cela, nous allons attaquer la '''micro-architecture''' du processeur, ce qu'il y a à l'intérieur et comment il fait pour exécuter une instruction. [[File:Von Neumann Cyclus.png|vignette|Les trois cycles d'une instruction.]] Le but d'un processeur, c'est d’exécuter des instructions. Cela nécessite de faire quelques manipulations assez spécifiques et qui sont toutes les mêmes quel que soit l'ordinateur. Exécuter une instruction est un processeur en trois étapes : * Le processeur charge l'instruction depuis la mémoire : c'est l'étape de '''chargement''' (''Fetch'') ; * Ensuite, le processeur « étudie » la suite de bits de l'instruction et en déduit quelle est l'instruction à exécuter : c'est l'étape de '''décodage''' (''Decode'') ; * Enfin, le processeur exécute l'instruction : c'est l'étape d’'''exécution''' (''Execute'').* Une quatrième '''étape d'interruption''' s'occupe des interruptions, précisément des interruptions matérielles et des exceptions. Elle est optionnelle, car de rares processeurs ne supportent pas les interruptions. Nous ne parlerons pas de l'étape d'interruption ici et allons nous concentrer sur les trois premières étapes. Il se trouve qu'elles sont réalisées chacune par un circuit séparé des autres. ==La micro-architecture d'un processeur== À l'intérieur du processeur, il y a un circuit dédié pour le chargement de l'instruction, un circuit dédié au décodage des instruction, et un autre pour leur exécution. Ils portent respectivement les noms d'unité de chargement, unité de décodage d'instruction, et de chemin de données. * Le '''chemin de données''' contient de quoi exécuter les instructions. Il regroupe des circuits de calcul, les registres, un circuit de communication avec la mémoire, et des interconnexions entre les circuits précédents. * L’'''unité de contrôle''' regroupe l'unité de chargement et le décodeur d'instruction. L'unité de chargement charge l'instruction depuis la mémoire, le décodeur/séquenceur commande le chemin de données. Les deux circuits sont séparés, mais ils communiquent entre eux pour gérer les branchements, pour charger les instructions au bon moment, etc. [[File:Microarchitecture d'un processeur.png|centre|vignette|upright=2|Microarchitecture d'un processeur]] ===Le chemin de données=== Pour effectuer des calculs, le processeur contient un circuit spécialisé : l''''unité de calcul'''. De plus, le processeur contient des '''registres''', ainsi qu'un circuit d''''interface mémoire'''. Les registres, l'unité de calcul, et l'interface mémoire sont reliés entre eux par un ensemble d'interconnexions, afin de pouvoir échanger des informations. Ces interconnexions forment ce qu'on appelle le '''bus interne du processeur'''. L'ensemble formé par ces composants s’appelle le '''chemin de données'''. Son nom vient du fait que les données circulent dans le chemin de données, dans le bus interne, l'unité de calcul, les registres et l'unité mémoire. Pour le dire autrement, il regroupe tous les circuits dans lesquels sont traités des données. [[File:Chemin de données.png|centre|vignette|upright=1.5|Chemin de données]] L'unité de calcul, les registres et l'unité mémoire sont configurables. Par exemple, l'unité de calcul peut faire plusieurs opérations : addition, soustraction, opérations logiques, et bien d'autres. Mais il faut préciser quelle opération effectuer. Pour cela, l'ALU a une entrée de commande sur laquelle on précise l'opération à effectuer. L'opération est encodée en binaire, chaque opération a son propre numéro. Il s'agit d'un '''signal de commande''', qui indique comment configurer l'unité de calcul pour faire ce qui est demandé. Les registres et l'unité mémoire ont aussi leurs propres signaux de commande dédiés. Par exemple, les noms/numéros de registres sont des signaux de commande qui permettent de sélectionner les registres utilisés comme opérande ou pour enregistrer le résultat. De même, l'unité mémoire ne fonctionne que si on lui précise quelle est l'adresse à lire/écrire, et s'il faut faire une lecture ou une écriture : les deux sont des signaux de commande. [[File:Signaux de commandes CPU.png|centre|vignette|upright=2|Signaux de commandes CPU]] ===Le séquenceur : signaux de commande et micro-opérations=== Le chemin de données est en charge de l'exécution des instructions, au minimum (on verra qu'il peut être impliqué dans l'étape de chargement, dans quelques paragraphes). Intuitivement, exécuter une instruction revient à configurer le chemin de données de manière adéquate. Si on veut faire une addition entre un registre et une adresse mémoire, on configure l'unité de calcul pour faire une addition, on configure les registres de manière à sélectionner les registres opérande/destination, et on envoie l'adresse à l'unité mémoire. Le '''décodeur d'instruction''' détermine quels sont les signaux de commande adéquats, à partir de l'instruction machine. Les signaux de commande des registres sont déterminés à partir des noms/numéros de registre encodés dans l'instruction, les signaux pour l'ALU sont déterminés à partir de l'opcode, etc. Le terme décodeur trahit le fait qu'il traduit une instruction machine en signaux de commandes séparés, qui se déduisent de l'instruction elle-même, de son encodage. [[File:Intérieur d'un processeur.png|centre|vignette|upright=2|Intérieur d'un processeur]] Mais cette explication ne marche que pour des instructions simples, qui, s'effectuent en une seule étape. Or, l'exécution d'une instruction complexe se fait en plusieurs étapes distinctes. Suivant l'instruction machine, le nombre d'étapes n'est pas le même. Voyons quelles instructions tendent à se faire une une ou plusieurs étapes, avec quelques exemples. Commençons par l'exemple d'une instruction de lecture en mode d'adressage absolu. L'adresse à lire est envoyée à l'unité mémoire directement, le nom de registre est envoyé aux registres et basta. L’exécution de l'instruction se fait donc en une seule étape : la lecture proprement dite. Maintenant, voyons la même instruction, mais avec un mode d'adressage base + indice. Avec ce mode d'adressage, l'adresse doit être calculée en additionnant une adresse de base et d'un indice, les deux étant stockés dans des registres. En plus de devoir lire la donnée, l'instruction va devoir calculer l'adresse, dans l'unité de calcul. L'étape d’exécution s'effectue dorénavant en deux étapes : une étape de calcul d'adresse, suivie de la lecture en mémoire. Bref, on voit bien que l’exécution d'une instruction s'effectue en plusieurs étapes distinctes, qui vont soit faire un calcul, soit échanger des données entre registres, soit communiquer avec la RAM. Chaque étape s'appelle une '''micro-opération''', ou encore une micro-instruction. Les micro-opérations se résument à des opérations basiques : échange de données entre registres, calcul sur l'ALU, accès mémoire. Chaque micro-opération encode les signaux de commande à destination du chemin de données, pour le configurer et le commander. Pour simplifier, une micro-opération est encodée en concaténant les signaux de commande pour l'ALU, ceux pour les registres, pour l'unité mémoire, etc. {|class="wikitable" |- ! colspan="4" | Micro-opération, encodage en binaire |- | Signaux de commande pour l'ALU | Signaux de commande pour les registres | Signaux de commande pour l'unité d'accès mémoire | Signaux de commande autres |} Toute instruction machine est équivalente à une suite de micro-opérations exécutée dans un ordre précis. Dit autrement, chaque instruction machine est traduite en suite de micro-opérations à chaque fois qu'on l’exécute. C'est le décodeur d'instruction qui transforme une instruction machine en une série de micro-opérations. Pour cela, le décodeur devient un circuit séquentiel. Il est parfois appelé le '''séquenceur''', terme qui trahit bien le fait qu'il traduit une instruction machine en une séquence de micro-opérations. [[File:Micro-operations.svg|centre|vignette|upright=2|Micro-operations]] Certaines µinstructions font un cycle d'horloge, alors que d'autres peuvent prendre plusieurs cycles. Un accès mémoire en RAM peut prendre 200 cycles d'horloge et ne représenter qu'une seule µinstruction, par exemple. Même chose pour certaines opérations de calcul, comme des divisions ou multiplication, qui correspondent à une seule µinstruction mais prennent plusieurs cycles. Typiquement, les instructions des processeurs RISC se font en une seule micro-opération, alors que les instructions complexes des processeurs CISC demandent plusieurs micro-opérations, particulièrement celles qui demandent de faire des accès mémoire ou qui utilisent des modes d'adressage complexes. Ce n'est pas une règle absolue, quelques instructions RISC se font en plusieurs micro-opérations, de même que les processeurs CISC ont des instructions simples qui se font en une seule micro-opération. Mais la corrélation est assez bonne. En conséquence, les processeurs RISC ont des décodeurs d'instruction très simples. ===L'unité de chargement=== [[File:Microarchitecture d'un processeur basique.png|vignette|Processeur basique, avec une unité de chargement.]] L''''unité de chargement''' est placée avant le décodeur/séquenceur. Son rôle est de charger les instructions les unes après les autres, dans le bon ordre. Pour exécuter une suite d'instructions dans le bon ordre, le processeur doit savoir quelle est la prochaine instruction à exécuter : il doit donc contenir une mémoire qui stocke cette information. C'est le rôle du registre d'adresse d'instruction, aussi appelé '''''program counter'''''. L'adresse de la prochaine instruction ne sort pas de nulle part : on peut la déduire de l'adresse de l'instruction en cours d’exécution par divers moyens plus ou moins simples. Généralement, on profite du fait que le programmeur/compilateur place les instructions les unes à la suite des autres en mémoire, dans l'ordre où elles doivent être exécutées. Ainsi, on peut calculer l'adresse de la prochaine instruction en ajoutant la longueur de l'instruction chargée au ''program counter''. Mais sur d'autres processeurs, chaque instruction précise l'adresse de la suivante. Ces processeurs n'ont pas besoin de calculer une adresse qui leur est fournie sur un plateau d'argent. Sur de tels processeurs, chaque instruction précise quelle est la prochaine instruction, directement dans la suite de bit représentant l'instruction en mémoire. Les processeurs de ce type contiennent toujours un registre d'adresse d'instruction, pour faciliter l’interfaçage avec le bus d'adresse. La partie de l'instruction stockant l'adresse de la prochaine instruction est alors recopiée dans ce registre, pour faciliter sa copie sur le bus d'adresse. Mais le compteur ordinal n'existe pas. Sur des processeurs aussi bizarres, pas besoin de stocker les instructions en mémoire dans l'ordre dans lesquelles elles sont censées être exécutées. Mais ces processeurs sont très très rares et peuvent être considérés comme des exceptions à la règle. [[File:Encodage d'une instruction sur un processeur sans Program Counter.png|centre|vignette|upright=2|Encodage d'une instruction sur un processeur sans Program Counter.]] L'unité de chargement s'occupe de tout ce qui a trait au ''program counter'', mais aussi de l'accès mémoire pour charger l'instruction. Il faut noter que sur certains processeurs, le chargement d'une instruction machine est une opération réalisée par le chemin de données. C'est le cas sur les architectures Von Neumann, où il n'y a qu'un seul bus mémoire, qui sert à la fois pour lire/écrire des données et pour charger des instructions. Dans ce cas, l'unité de communication avec la mémoire s'occupe aussi de charger les instructions. Le chargement d'une instruction est alors réalisée par une micro-opération d'accès mémoire, qui copie l'instruction chargée dans un registre dédié, appelé le registre d'instruction, lui-même relié au séquenceur. ===Les autres circuits=== Un processeur contient au minimum une unité de chargement, le chemin de données et le décodeur d'instruction. Mis c'est là le minimum, un processeur peut parfaitement contenir d'autres circuits en plus. Par exemple, il peut intégrer des circuits pour gérer les interruptions matérielles, des circuits ''timer'' pour compter des durées, des circuits dit DMA pour communiquer avec les périphériques, etc. L'exemple du 80186 d'Intel est illustré ci-dessous. Il est composé de deux circuits principaux : une ''Execution Unit'' qui regroupe l'unité de calcul et les registres, une ''Bus Interface Unit'' qui regroupe unité mémoire, unité de chargement et séquenceur. Mais en plus de ces deux circuits principaux, le processeur incorpore des circuits ''timers'', de quoi gérer les interruption, un circuit DMA, et bien d'autres encore. [[File:Intel 80186 80188 arch.svg|centre|vignette|upright=2.5|Intel 80186 80188 arch]] ==Des processeurs vendus en kit aux premiers microprocesseurs== Un processeur est un circuit assez complexe et qui utilise beaucoup de transistors. Avant les années 1970, il n'était pas possible de produire un processeur en un seul morceau. Impossible de mettre un processeur dans un seul boitier. Les tout premiers processeurs étaient fabriqués porte logique par porte logique et comprenaient plusieurs milliers de boitiers reliés entre eux. Par la suite, les progrès de la miniaturisation permirent de faire des pièces plus grandes. L'invention du microprocesseur permis de placer tout le processeur dans un seul boitier, une seule puce électronique. ===Avant l'invention du microprocesseur=== Avant l'invention du microprocesseur, les processeurs étaient fournis en pièces détachées qu'il fallait relier entre elles. Le processeur était composé de plusieurs circuits intégrés, placés sur la même carte mère et connectés ensemble par des fils métalliques. Un exemple de processeur conçu en kit est la série des Intel 3000. Elle regroupe plusieurs circuits séparés : l'Intel 3001 est le séquenceur, l'Intel 3002 est le chemin de données (ALU et registres), le 3003 est un circuit d'anticipation de retenue censé être combiné avec l'ALU, le 3212 est une mémoire tampon, le 3214 est une unité de gestion des interruptions, les 3216/3226 sont des interfaces de bus mémoire. On pourrait aussi citer la famille de circuits intégrés AMD Am2900. Les ALUs en pièces détachées de l'époque étaient assez simples et géraient 2, 4, 8 bits, rarement 16 bits. Et il était possible d'assembler plusieurs ALU pour créer des ALU plus grandes, par exemple combiner plusieurs ALU 4 bits afin de créer une unité de calcul 8 bits, 12 bits, 16 bits, etc. Il s'agit de la méthode du '''''bit slicing''''' que nous avions abordée dans le chapitre sur les unités de calcul. ===L'intel 4004 : le premier microprocesseur=== Par la suite, les progrès de la miniaturisation ont permis de mettre un processeur entier dans un seul circuit intégré. C'est ainsi que sont nés les '''microprocesseurs''', à savoir des processeurs qui tiennent tout entier sur une seule puce de silicium. Les tout premiers microprocesseurs étaient des processeurs à application militaire, comme le processeur du F-14 CADC ou celui de l'''Air data computer''. Le tout premier microprocesseur commercialisé au grand public est le 4004 d'Intel, sorti en 1971. Il comprenait environ 2300 transistors, avait une fréquence de 740 MHz, et manipulait des entiers de 4 bits. De plus, le processeur manipulait des entiers en BCD, ce qui fait qu'il pouvait manipuler un chiffre BCD à la fois (un chiffre BCD est codé sur 4 bits). Il pouvait faire 46 opérations différentes. C'était au départ un processeur de commande, prévu pour être intégré dans la calculatrice Busicom calculator 141-P, mais il fut utilisé pour d'autres applications quelque temps plus tard. Son successeur, l'Intel 4040, garda ces caractéristiques et n'apportait que quelques améliorations mineures : plus de registres, plus d'opérations, etc. Immédiatement après le 4004, les premiers microprocesseurs 8 bits furent commercialisés. Le 4004 fut suivi par le 8008 et quelques autres processeurs 8 bits extrêmement connus, comme le 8080 d'Intel, le 68000 de Motorola, le 6502 ou le Z80. Ces processeurs utilisaient là encore des boitiers similaires au 4004, mais avec plus de broches, vu qu'ils étaient passés de 4 à 8 bits. Par exemple, le 8008 utilisait 18 broches, le 8080 était une version améliorée du 8008 avec 40 broches. Le 8086 fut le premier processeur 16 bits. ===L'évolution des processeurs dans le temps=== La miniaturisation a eu des conséquences notables sur la manière dont sont conçus les processeurs, les mémoires et tous les circuits électroniques en général. On pourrait croire que la miniaturisation a entrainé une augmentation de la complexité des processeurs avec le temps, mais les choses sont à nuancer. Certes, on peut faire beaucoup plus de choses avec un milliard de transistors qu'avec seulement 10000 transistors, ce qui fait que les puces modernes sont d'une certaine manière plus complexes. Mais les anciens processeurs avaient une complexité cachée liée justement au faible nombre de transistors. Il est difficile de concevoir des circuits avec un faible nombre de transistors, ce qui fait que les fabricants de processeurs devaient utiliser des ruses de sioux pour économiser des transistors. Les circuits des processeurs étaient ainsi fortement optimisés pour économiser des portes logiques, à tous les niveaux. Les circuits les plus simples étaient optimisés à mort, on évitait de dupliquer des circuits, on partageait les circuits au maximum, etc. La conception interne de ces processeurs était simple au premier abord, mais avec quelques pointes de complexité dispersées dans toute la puce. De nos jours, les processeurs n'ont plus à économiser du transistor et le résultat est à double tranchant. Certes, ils n'ont plus à utiliser des optimisations pour économiser du circuit, mais ils vont au contraire utiliser leurs transistors pour rendre le processeur plus rapide. Beaucoup des techniques que nous verrons dans ce cours, comme l’exécution dans le désordre, le renommage de registres, les mémoires caches, la présence de plusieurs circuits de calcul, et bien d'autres ; améliorent les performances du processeur en ajoutant des circuits en plus. De plus, on n'hésite plus à dupliquer des circuits qu'on aurait autrefois mis en un seul exemplaire partagé. Tout cela rend le processeur plus complexe à l'intérieur. Une autre contrainte est la facilité de programmation. Les premiers processeurs devaient faciliter au plus la vie du programmeur. Il s'agissait d'une époque où on programmait en assembleur, c'est à dire en utilisant directement les instructions du processeur ! Les processeurs de l'époque utilisaient des jeu d'instruction CISC pour faciliter la vie du programmeur. Pourtant, ils avaient aussi des caractéristiques gênantes pour les programmeurs qui s'expliquent surtout par le faible nombre de transistors de l'époque : peu de registres, registres spécialisés, architectures à pile ou à accumulateur, etc. Ces processeurs étaient assez étranges pour les programmeurs : très simples sur certains points, difficiles pour d'autres. Les processeurs modernes ont d'autres contraintes. Grâce à la grande quantité de transistors dont ils disposent, ils incorporent des caractéristiques qui les rendent plus simples à programmer et à comprendre (registres banalisés, architectures LOAD-STORE, beaucoup de registres, moins d'instructions complexes, autres). De plus, si on ne programme plus les processeurs à la main, les langages de haut niveau passe par des compilateurs qui eux, programment le processeur. Leur interface avec le logiciel a été simplifiée pour coller au mieux avec ce que savent faire les compilateurs. En conséquence, l’interface logicielle des processeurs modernes est paradoxalement plus minimaliste que pour les vieux processeurs. Tout cela pour dire que la conception d'un processeur est une affaire de compromis, comme n'importe quelle tâche d'ingénierie. Il n'y a pas de solution parfaite, pas de solution miracle, juste différentes manières de faire qui collent plus ou moins avec la situation. Et les compromis changent avec l'époque et l'évolution de la technologie. Les technologies sont toutes interdépendantes, chaque évolution concernant les transistors influence la conception des puces électroniques, les technologies architecturales utilisées, ce qui influence l'interface avec le logiciel, ce qui influence ce qu'il est possible de faire en logiciel. Et inversement, les contraintes du logiciel influencent les niveaux les plus bas, et ainsi de suite. Cette morale nous suivra dans le reste du cours, où nous verrons qu'il est souvent possible de résoudre un problème de plusieurs manières différentes, toutes utiles, mais avec des avantages et inconvénients différents. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Les interruptions et exceptions | prevText=Les interruptions et exceptions | next=Le chemin de données | nextText=Le chemin de données }} </noinclude> lr8ebq0pj5hv9rq2k99ue7l1caabxff Fonctionnement d'un ordinateur/Les processeurs superscalaires 0 65956 772668 772649 2026-09-21T13:53:56Z Mewtow 31375 772668 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 superscalaire à N voies'''. [[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]] Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit 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 tout premiers processeurs superscalaires pouvaient émettre deux µops en même temps, ce qui portait le nom de '''double émission'''. Mais le nombre a rapidement augmenté et les processeurs modernes 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 wikilivres, 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. ==L'implémentation des processeurs superscalaires== Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre. Intuitivement, émettre et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Il est aussi possible de dupliquer les unités de calcul pour exécuter plusieurs µops en même temps. Mais d'autres circuits sont simplement modifiés. Par exemple, l'unité de chargement n'est pas dupliquée, mais simplement modifiée, idem pour l'unité d'émission et les circuits d'exécution dans le désordre. Voyons ce qu'il en est dans le détail. ===Les unités de calcul d'un processeur superscalaire=== Intuitivement, on se dit que les unités de calcul doivent être dupliquées. Par exemple, un processeur à double émission devrait dupliquer chaque ALU et FPU en deux exemplaires (N exemplaires pour un CPU à N voies). L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission. Le problème, c'est que le cout en circuit est énorme ! Faire cela revient à dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, etc. Un autre cas extrême est celui où aucune unité de calcul n'est dupliquée. Pour comprendre comment c'est possible rappelez-vous qu'un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et éventuellement une unité de branchement. Cela suffit pour créer un processeur quadruple émission, très limité. Un tel processeur peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire. Le problème est qu'il est rare que l'on ait à exécuter en même temps un calcul entier, un calcul flottant, un accès mémoire et un branchement. La quadruple émission n'est donc que rarement utilisée. En pratique, les seules combinaisons fréquentes sont les combinaisons calcul + accès mémoire et calcul + branchement. Les combinaisons calcul entier + calcul flottant sont aussi possibles, mais plus rares. Mais on dépasse rarement la double émission. De plus, au-delà de la quadruple émission, on ne peut plus se contenter d'une ALU, d'une FPU, d'une unité de branchement et d'une unité mémoire. Les processeurs superscalaires larges n'ont pas le choix que de rajouter des unités de calcul, voire des unités mémoire. Reste à voir ce qui est dupliqué en pratique. En pratique, les CPU superscalaires sont un intermédiaires entre ces deux cas extrêmes : certaines unités de calcul sont dupliquées en plusieurs exemplaires, mais pas toutes. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant. La conséquence est que les processeurs superscalaires ont des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a des paires d'instructions autorisées et des paires interdites. Par exemple, l'exécution simultanée de deux branchements est interdite, les branchements sont exécutés l'un après l'autre. Mais il est possible d'émettre un branchement en même temps qu'une autre instruction, en espérant que la prédiction de branchement ait fait une bonne prédiction. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur. En pratique, ces contraintes d’appariement ont peu d’impact. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière, le ratio est le même quand on remplace la multiplication par un branchement ou un accès mémoire. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Deux branchements consécutifs sont quant à eux plus fréquents, idem pour deux accès mémoire. Dupliquer des ALU entières est donc la priorité, suivi par la duplication des unités de branchement, puis celle des accès mémoire, pour terminer par le multiplieur et la FPU. ===Le chargement de plusieurs instructions consécutives=== Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large. Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence. Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas. Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement non-pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc. [[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]] ===Le décodage parallèle des instructions=== Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits. Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur. Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! ===L'unité d'émission d'un processeur superscalaire=== La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. Nous verrons pourquoi dans la suite, mais on peut dire que c'est juste plus simple et plus efficace ainsi. Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop candidate, sous entendu "candidate à l'émission". L'unité d'émission détecte les dépendances entre la µop candidate et les µops en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en cours d'exécution dans le pipeline. mais en plus, elle doit tester les dépendances entre les µops candidates. Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément, il doit détecter les paires d'instructions dépendantes et les sérialiser. Avec une fenêtre d'instruction, il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil gérent les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires modernes utilisent tous une fenêtre d'instruction centralisée ou décentralisée, et non des ''scoreboard''. Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps, là encore en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire peut émettre 8 µops à la fois, on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle. ===L'unité de renommage superscalaire=== Avec la superscalarité, l'unité de renommage de registre doit gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance de registre, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées. Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat. [[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]] Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante. [[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]] Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande. [[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]] ===Le banc de registre d'un processeur superscalaire=== Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement. Un autre solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Pour cela, le processeur doit détecter quand il n'y a pas assez de ports pour servir toutes les instructions. L'unité d'émission devra alors mettre en attente certaines instructions, le temps que les ports se libèrent. Cette détection est réalisée par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter''). Un autre détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Et cette différence a une influence le nombre de ports de lecture du banc de registre. Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec une fenêtre d'instruction, il faut le double du nombre de ports d'émission. Mais avec des stations de réservation, il faut le double des ports de décodage. La raison est que les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation (sous-entendu, après le décodage). or, comme dit plus haut, les processeurs superscalaire larges ont souvent plus de ports d'émission que de décodage. ===L’impact de la superscalarité sur les autres circuits=== Pour résumer le tout, voici ce que cela donne dans les grandes lignes. * L'unité de chargement charge deux fois plus de données, mais n'est pas modifiée en profondeur. * Les décodeurs sont dupliqués, ce qui a un cout en circuit pas négligeable. * Les circuits d'émission voient leur nombre de ports doubler/tripler/quadrupler. * L'unité de renommage de registre, le ROB et les circuits d’exécution dans le désordre sont légèrement altérés. * Les unités de calcul peuvent être dupliquées, mais ce n'est pas systématique. * Le banc de registre et l'unité mémoire ne sont pas forcément modifiés, mais s'ils le sont, c'est pour leur ajouter des ports. {|class="wikitable" |- ! rowspan="2" | Processeur sans émission multiple | rowspan="2" | Chargement | rowspan="2" | Décodage | rowspan="2" | Renommage | rowspan="2" | Émission | Exécution / ALU | rowspan="2" | ''Commit''/ROB |- | Exécution / ALU |- ! rowspan="4" | Processeur superscalaire | rowspan="4" | Chargement | rowspan="2" | Décodage | rowspan="4" | Renommage | rowspan="4" | Émission | Exécution / ALU | rowspan="4" | ''Commit''/ROB |- | Exécution / ALU |- | rowspan="2" | Décodage | Exécution / ALU |- | Exécution / ALU |} Pour les unités de calcul, tout dépend du processeur. Il existe même des processeurs sur lesquels aucune unité de calcul n'est ajoutée ! Et pour comprendre pourquoi, nous allons parler des processeurs superscalaires historiques. ==L'évolution historique des processeurs superscalaires== Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs triple émission permettent eux d’émettre trois instructions à la fois. Les tout premiers processeurs superscalaires avaient un budget en transistors très limité, ce qui fait qu'ils ne pouvaient pas être "totalement superscalaires". Par exemple, un processeur double émission ne pouvait pas émettre deux opérations entières en même temps, ni deux opérations flottantes, ni une opération entière avec un accès mémoire. Il y avait des paires d'instructions autorisées et des paires interdites. Les '''contraintes d'appariement''' sur les instructions à émettre en même temps étaient très strictes. L'intérêt de ces contraintes d’appariement est que le processeur était très simple, il n'y avait pas besoin de rajouter des circuits complexes pour rendre le processeur superscalaire. Les CPU superscalaires historiques ne faisaient que mieux utiliser des unités de calcul déjà présentes. Prenons un processeur non-superscalaire simple, illustré ci-dessous. Il contient : une unité de chargement, un décodeur, une unité d'émission, une ALU, une FPU, les bancs de registre entier et flottants, et une unité mémoire. Nous avons placé l'unité mémoire à la suite de l'ALU entière, car ce sera important pour les explications qui vont suivre. [[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]] Les processeurs superscalaires historiques modifiaient assez peu le schéma précédent. L'unité de chargement était adaptée pour lire plusieurs instructions depuis la mémoire, il y avait un seul décodeur, l'unité d'émission était assez peu modifiée. Mais une chose était certaine : aucune unité de calcul n'était dupliquée, le banc de registre n'était presque pas modifié. A la rigueur, gérer la triple 'émission avec les branchements demandait parfois d'ajouter des ports de lecture sur le banc de registre généraux, pas plus. ===La double émission entière-flottante=== Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est d'émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Le processeur a donc deux pipelines séparés : un pour les micro-opérations entières, un autre pour les micro-opérations flottantes. On parle alors de '''double émission entière-flottante'''. La mise en œuvre utilise assez peu de circuits, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité. La plupart des premiers processeurs superscalaires étaient de ce type. Les exemples les plus notables sont les processeurs POWER 1 et ses dérivés comme le ''RISC Single Chip''. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle. [[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]] La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisation visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate]. ===L'émission parallèle des branchements=== Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur. L'idée était d'appliquer un équivalent de la double émission, non pas pour des opérations flottantes, mais pour autre chose. Il y avait deux candidats : l'unité de branchement, l'unité mémoire. Et c'est l'unité de branchement qui a été choisie. Le processeur pouvait émettre un branchement en parallèle d'une instruction entière. La technique s'appelle l''''émission parallèle des branchements'''. Combinée à la double émission entière-flottante, cela permettait d'émettre trois µops en même temps : un branchement, une opération flottante, et une opération entière/mémoire. Les exemples de processeurs de ce type ne sont pas très nombreux, mais on peut citer l'Intel i960CA et l'HyperSPARC. Les branchements étant fréquents, surtout sur les CPU de type RISC, les gains en performances étaient assez impressionnants. Quant au cout en transistor, il dépendait du jeu d’instruction, expliquons pourquoi. Pour les processeurs avec un registre d'état, l'implémentation est particulièrement simple. L'unité de branchement lit ce registre d'état, sélectionne le bit adéquat, teste sa valeur, et modifie le ''program counter'' en fonction. Le processeur intègre déjà une unité de branchement séparée de l'unité de calcul, pour faire tout cela. Pas besoin d'utiliser l'unité de calcul entière, ni toute autre unité. Il n'y a même pas besoin de toucher au banc de registres, vu que le registre d'état n'est pas dedans, idem pour le ''program counter'' Et il en est de même si le processeur utiliser des registres à prédicats. Là encore, il y a un banc de registre séparé pour les prédicats et une unité de branchement dédiée. L'implémentation de la triple émission ne fait alors qu'utiliser au mieux des circuits existants. Sur les CPU de type RISC, sans registre d'état, les choses sont cependant différentes. De tels processeurs ont des instructions de branchements qui lisent deux opérandes dans les registres, les comparent, et altèrent ou non le ''program counter'' suivant le résultat. L'implémentation de la triple émission demande alors d'ajouter deux ports de lecture au banc de registre entier, pour lire les opérandes. Il faut aussi ajouter une unité pour faire la comparaison, si elle n'était pas déjà présente. Certains CPU RISC utilisent une unité de branchement séparée, mais d'autres utilisaient l'ALU entière pour. ===L'émission multiple des micro-opérations entières=== Voyons maintenant l'étape après la triple émission entière-flottante-branchement. Elle est assez simple, à savoir qu'elle permet d'émettre deux instructions entières en même temps, avec éventuellement une instruction flottante en plus. Il est parfois possible de monter à trois ou quatre instructions entières. Le processeurs de ce type font de l''''émission multiple des micro-opérations entières'''. Nous utiliserons le terme d'''émission entière multiple'' pour simplifier les explications. Pour cela, les unités de calcul entières doivent être dupliquées. Il doit y avoir deux ALU entières pour de la double émission, trois pour de triple émission, etc. De plus, il faut modifier le banc de registres. Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. [[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]] Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications et les divisions ? Pour étudier ces questions, nous allons prendre le cas d'un processeur capable d’émettre trois opérations en même temps : deux opérations entières, et une flottante, potentiellement d'autres. Le cas des opérations complexes est assez intéressant. Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les ALUs en N exemplaires. Mais ce n'est jamais fait en pratique. Concrètement, seules les ALU entières simples le sont, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ne sont pas dupliqués en N exemplaires, le cout en transistors serait trop important. Pour un processeur à double émission, l'unité d'émission a deux ports d'émission, pour émettre deux µops entières. Les deux ports ont chacun leur ALU entière, mais l'un d'entre eux a est aussi relié au circuit multiplier. En clair, cela permet d'émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Idem pour les opérations simples comme les soustractions, les opérations bit à bit, etc. On peut en exécuter deux, ou en coupler une avec une multiplication. Faire ainsi n'a pas de cout en performance significatif, comparé à une duplication totale des ALUs entières. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur serait inutile. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle. [[File:Emission multiple des opérations entières, implémentation naive.png|centre|vignette|upright=2|Émission multiple des opérations entières, implémentation naïve.]] Il en est de même avec les décalages, vu que le ''barrel shifter'' n'est pas dupliqué. Il est donc possible d'émettre deux opérations de base, un décalage avec une opération de base, mais pas deux décalages en parallèle. En général, le ''barrel shifter'' et le multiplieur sont placés sur deux ports d'émission séparés. Cela permet ainsi de faire un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas. [[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Emission multiple des opérations entières, implémentation courante]] Pour les branchements, deux solutions sont possibles. Avec la première, il y a émission parallèle des branchements, à savoir qu'il est possible d'émettre en branchement en parallèle de deux-trois instructions entières. Et cela impose la présence d'une unité de branchement séparée, avec son port d'émission. Avec la seconde, les branchements sont traités comme des instructions entières. Il peut y avoir une unité de branchement séparée, mais ce n'est pas souvent le cas. En général, les branchements sont exécutés dans une ALU entière. [[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Emission série et parallèle des branchements]] Les accès mémoire sont traités comme des opérations entières un peu spéciales, sur les processeurs superscalaires historiques. En clair, le processeur peut émettre deux opérations entières, ou une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent. Les raisons à cela étaient nombreuses. Et ces raisons sont encore valables sur des petits processeurs destinés à l'embarqué ou l'informatique industrielle. Seuls les processeurs haute performance modernes font différemment. La raison principale est le cout en circuit. Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse. Pas besoin de rajouter une unité de calcul d'adresse séparée. Mais surtout, permettre l'émission parallèle des accès mémoire ferait passer un processeur triple émission à de la quadruple émission, avec tous les couts que ca implique. Il faudrait rajouter des ports de lecture/écriture au banc de registre généraux, pour traiter un accès mémoire en plus du reste. Les circuits d'émission et de chargement devraient être élargit, aussi. Et quitte à payer un tel cout en circuit, il serait plus rentable de l'utiliser pour émettre une quatrième opération entière qu'un accès mémoire. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2. ===Les CPU superscalaires à plusieurs unités spécialisées=== Le classique deux opérations entières + une flottante était l'idéal pour la triple émission. Même les processeurs modernes à triple émission restent sur cette "norme", car elle est proche de l'optimal. Mais arrivé à la quadruple émission, les possibilités sont devenus plus diverses. L'évolution des CPU superscalaires ont évolués dans des directions différentes, chacun faisant à sa sauce, les choses sont devenus plus confuses. Le seul point commun est qu'ils ont eu tendance à rajouter des unités de calcul. Un point important est que la quadruple émission permet de nombreuses possibilités, mais la plupart ne sont pas intéressantes. Pour donner un exemple concret, prenons un CPU superscalaire très simple, comprenant une ALU entière, une FPU, une unité de branchement, et une unité mémoire. Il est possible de créer un CPU quadruple émission avec ces unités, mais le gain en performance n'en vaudrait pas la peine pour le cout en circuits associé. Il est rare qu'un bloc de 4 instructions contienne exactement une opération entière, une flottante, un branchement et un accès mémoire. En pratique, les seules combinaisons fréquentes sont les combinaisons calcul + accès mémoire et calcul + branchement. Les combinaisons calcul entier + calcul flottant sont aussi possibles, mais plus rares. Mais on dépasse rarement la double émission. Il est préférable d'utiliser ces unités dans un CPU double ou triple émission, en retirant l'unité de branchement et/ou en couplant l'unité mémoire à l'ALU. En clair, une fois arrivé à la quadruple émission, on ne peut plus se contenter d'une ALU, d'une FPU, d'une unité de branchement et d'une unité mémoire. Les processeurs superscalaires larges n'ont pas le choix que de rajouter des unités de calcul, voire des unités mémoire. Et en pratique, ils ont privilégié l'ajout d'ALU entières, avant de dupliquer les autres circuits. La raison est que les programmes informatiques utilisent beaucoup plus d'additions/soustractions/comparaisons que le reste. Il y a environ 5 opérations entières de base pour une multiplication entière, le ratio est le même quand on remplace la multiplication par un branchement ou un accès mémoire. En conséquence, les ALU entières ont été dupliquées en premier, et dans de nombreux exemplaires. Reste à voir comment a été dupliqué le reste. Les multiplieurs n'ont pas été dupliqués, sauf sur les processeurs superscalaires très larges. La raison est qu'il est rare de devoir exécuter deux multiplications à la fois. C'est la même chose pour le ''barrel shifter''. Deux branchements consécutifs sont quant à eux plus fréquents, c'est même assez courant. Aussi, les unités de branchement ont été dupliquées en second lieu, après les ALU entières. Avoir plusieurs accès mémoire proches est assez courant, surtout sur les CPU de type CISC. Aussi, les unités mémoire ont été dupliquées en troisième lieu. Pour résumer, dupliquer des ALU entières est donc la priorité, suivi par la duplication des unités de branchement, puis celle des accès mémoire, pour terminer par le multiplieur. La FPU est un peu à part. Une autre tendance à été de séparer l'unité mémoire des ALU entières, ce qui a permis l''''émission parallèle des µops mémoire'''. Comme son nom l'indique, l'idée est de séparer les µops mémoire des opérations entières. Il devient alors possible d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière. La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple. La '''double émission flottante''' a été utilisée sur les processeurs Alpha. L'idée est d'émettre deux opérations flottantes à la fois. Sauf qu'une implémentation naive demanderait de dupliquer la FPU, ce qui aurait un cout en circuit rédhibitoire. Pour rappel, les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant. l'idée est d'avoir des ports d'émission séparés pour l'additionneur flottant et le multiplieur flottant. Le processeur peut alors émettre une addition flottante en même temps qu'une multiplication flottante. Les autres circuits de calcul flottant sont répartis sur ces deux ports d'émission flottants. L'avantage est que cela se marie bien avec l'usage d'une fenêtre d'instruction séparée pour les opérations flottantes. La fenêtre d'instruction a alors deux ports séparés, au lieu d'un seul. Rajouter un second port d'émission flottant n'est pas trop un problème, car le cout lié à l'ajout d'un port n'est pas linéaire. Passer de un port à deux a un cout tolérable, bien plus que de passer de 3 ports à 4 ou de 4 à 5. Il est possible de dupliquer l'additionneur et le multiplieur flottant, ce qui permet d'émettre deux additions et multiplications flottantes en même temps. C'est ce qui est fait sur les processeurs AMD de architecture Zen 1 et 2. Ils ont deux additionneurs flottants par cœur, deux multiplieurs flottants, chacun avec leur propre port d'émission. Les performances en calcul flottant sont assez impressionnantes pour un processeur de l'époque. [[File:ZEN - émission multiple flottante.png|centre|vignette|upright=2|Microarchitecture Zen 1 d'AMD.]] ==Les ports d'émission et de décodage== Les ports d'émission et de décodage sont des ressources matérielles comme les autres. Dans cette section, nous allons parler de concepts liés aux ports d'émission et de décodage. Nous allons notamment parler du partage des ports d'émission, ainsi que des relations numériques entre ces deux types de ports. ===Le partage des ports d'émission et la ''port contention''=== Il est possible de partager un port d'émission, à savoir de connecter un port d'émission à plusieurs unités de calcul. Nous en avons vu un exemple plus haut, avec l'émission multiple des opérations entières. On peut par exemple connecter un multiplieur et une ALU entière sur le même port d'émission. Pour le moment, nous n'avons vu que des cas où un port d'émission est relié soit à des ALU entières, soit à des FPU, pas aux deux. Mais il existe des processeurs où un même port d'émission alimente à la fois une ALU entière et une FPU. Par exemple, on peut relier un additionneur flottant sur le même port qu'une ALU entière. Il faut noter que cela implique une fenêtre d'instruction centralisée, capable de mettre en attente micro-opérations entières et flottantes. Un exemple est celui des processeurs Core 2 Duo. Ils disposent de 6 ports d'émission, dont 3 ports dédiés à l'unité mémoire. Les 3 ports restants alimentent chacun : une ALU entière, un circuit de calcul flottant et une unité de calcul SSE (une unité de calcul SIMD, qu'on abordera dans quelques chapitres). * Le premier port alimente une ALU entière simple et un multiplieur/diviseur flottant. * Le second alimente une ALU entière, un multiplieur entier et un additionneur flottant. * Le troisième alimente une ALU entière, sans circuit flottant dédié. Dans le même genre, il est possible de partager un port d'émission entre l'unité mémoire et une ALU entière. Cela permet d'utiliser l'ALU entière pour les calculs d'adresse, ce qui évite d'avoir à utiliser une unité de calcul d'adresse distincte. Un exemple est celui du processeur superscalaire double émission Power PC 440. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. L'organisation en question permet soit d'émettre un accès mémoire en même temps qu'une opération entière, soit d'émettre deux opérations entières simples, soit d’émettre une multiplication et une addition/soustraction/comparaison. Une organisation simple, mais très efficace ! [[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]] Regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles, qui ne se présentent que pour certaines combinaisons de micro-opérations bien précises. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port. Le nombre de ports peut être un facteur limitant pour la performance dans certaines situations de '''''port contention''''' où on a assez d'ALU pour exécuter N micro-opérations, mais où la répartition des ALUs sur les ports l’empêche. Mais on peut réduire la ''port contention'' en répartissant correctement les ALU sur les ports d'émission. Le choix en question dépendent fortement du fait qu'en moyenne, que certaines paires d'instructions sont plus fréquentes que d'autres. Tout cela guide ce choix de répartition des ALu sur les ports. Nous venons de voir que la répartition des ALU sur les ports d'émission est très variable d'un processeur à l'autre. Les divers choix possibles sont tous des compromis entre deux forces : réduire le nombre de ports d'émission d'un côté, garder de bonnes performances en réduisant la ''port contention'' de l'autre. Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction relativement simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ? Par contre, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal répartit les ALU. ===La séparation entre largeur de décodage et d'émission=== Sur les processeurs superscalaires larges, le processeur peut émettre plus d'instructions qu'il peut en décoder. Plus haut, j'ai dit que l'unité d'émission avait parfois plus de ports d'émission que de ports de décodage. La raison est assez simple, et elle s'explique assez bien avec un exemple. Prenons un processeur avec trois ALU entières, une FPU, une unité de branchement et une unité mémoire. Chacune a son propre port d'émission, ce qui fait que le CPU est capable d'émettre 6 µops. Du moins, c'est la théorie, car les conditions pour sont assez rares. Il faut qu'en chargeant un paquet de 6 instructions, il y ait exactement trois instructions entières, une flottante, un branchement et un accès mémoire. Si le paquet contient 5 instructions entières et un branchement, seules quatre instructions sont émises. Dans cet exemple, dans la grande majorité des cas, seuls 4 ports seront utilisés en moyenne et deux seront sous-utilisés. Il s'agit là d'une moyenne, qui dépend du code, et des instructions exactes à exécuter. Cependant, cette moyenne nous donne un moyen d'économiser beaucoup de circuits pour un cout en performance mineur. L'idée est de ne charger que des blocs de 4 instructions, mais d'avoir 6 ports d'émission. Le processeur charge et décode 4 instructions, les quatre instructions sont envoyées sur les ports d'émission adéquat. Le cout en transistors est alors situé entre celui d'un CPU quadruple émission et sextuple émission. Et les performances sont aussi entre les deux. Pour les performance c'est parce que cela permet d'avoir un CPU quadruple émission avec bien moins de contraintes d’appariement. Imaginez qu'on se soit limité à quatre ports d'émission : il aurait fallu mettre plusieurs unités sur le même port d'émission. En conséquence, il y aurait eu des combinaisons interdites, comme émettre deux instructions entières + un branchement + un accès mémoire ; ou deux instructions entières + une opération flottante + un accès mémoire. De tells appariement interdits sont possibles en utilisant les six ports d'émission. ==L'émission multiple des accès mémoire== Les premiers processeurs superscalaires ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'une calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles. Par contre, c'est autre chose sur les processeurs superscalaires larges. Vu qu'ils travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares. L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. La ''load/store queue'' reste une structure unique, mais on peut lui rajouter des ports. Et à ce peit jeu, il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire. La première duplique les unités de calcul, mais pas les ports d'accès au cache. Les accès mémoire sont alors accumulés dans la ''load/store queue'', qui sont ensuite traités un par un par le cache. Un processeur utilisant cette technique est, par exemple, capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. Un exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. Ces processeurs pouvaient calculer trois adresses simultanément, mais ne pouvaient faire qu'un seul accès au cache par cycle. Le cout en circuit est modéré : les unités de calcul coutent très peu, les ports de la ''load/store queue'' coutent déjà un peu plus. La seconde méthode ajoute des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultanées. Il faut alors dupliquer des unités de calcul, rajouter les ports sur le cache, mais aussi rajouter des ports sur la ''load/store queue'', pour qu'elle échange des données/commandes mémoire avec le cache. Le cout en circuits est alors conséquent Quelques processeurs utilisaient des solutions intermédiaires, un exemple classique étant les Pentium 1 et 2 d'Intel. Mais nous en parlerons dans le chapitre suivant. [[File:Processeur non-superscalaire basique 02.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]] Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre. Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Le Pentium 4 était dans ce cas, mais il était un peu seul dans sa catégorie. Un cas similaire est celui où l'unité mémoire est alimentée par une fenêtre de micro-opération mélangeant instructions mémoire et entières. Les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. Dans ce cas, l'émission multiple est rarement implémentée. Le cas suivant a une file pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique. Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui pouvaient seulement émettre une lecture en même temps qu'une écriture. Ils avaient deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse. [[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]] Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture demande d'utiliser une file d'écriture multiport, ce qui est assez compliqué. Implémenter des lectures en plus est plus simple : il suffit de rajouter une seconde file de µops LOAD et une unité de calcul, alimentées via un second port d'émission. Il est aussi possible de rajouter un second port de lecture au cache, mais ce n'est pas obligatoire. Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse. De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports de lecture et d'écriture car il y a plus de lecture que d'écritures. Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. : Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse. [[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]] Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettent plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail. Pour résumer, l'évolution des CPU superscalaires a été la suivante : * pipeline mémorie fusionné avec le pipeline entier ; * séparation du pipeline mémoire du reste, avec émission parallèle des accès mémoire ; * émission multiple, mais sans dupliquer les ports du cache ; * émission multiple, avec émission simultanée d'une lecture et d’une écriture dans le cache ; * émission multiple avec plusieurs accès en lecture au cache. ==Les optimisations des processeurs superscalaires larges== Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul. ===L'étape de chargement d'un CPU superscalaire large=== Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 instruction simultanées. Pour rappel, la stratégie basique pour charger N instructions est de charger un bloc de N instructions consécutives. Les instructions après un branchement ne sont pas décodées ni exécutées, ce qui entraine une petite baisse de performance. Pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient suffisamment importante pour que la conception du CPU doit alors être repensée de fond en comble. Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part. La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime. La solution précédente joue sur la distinction entre pots de décodage et d'émission. Elle permet d'émettre un grand nombre de µops sans pour autant charger un grand nombre d'instructions en même temps. En quelque sorte, ce n'est qu'une méthode de contournement, le CPU superscalaire reste de type étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements. Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées. [[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]] Un ''front-end'' large charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles. [[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions autoaligné.]] Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements ! [[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]] Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez long, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''. ===Le décodage superscalaire large=== Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle. Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total). Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instruction décodée est un branchement, alors le second ''cluster'' décode les trois instructions situés après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''. Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle. Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''. Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''. ===Le contournement sur les processeurs superscalaires larges=== Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. Il faut aussi des comparateurs pour détecter des dépendances de données. [[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]] La sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec. Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup d’interconnexions et de comparateurs. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable au-delà. Non seulement ce serait un enfer à câbler, mais les fils seraient tellement longs que la fréquence du processeur en prendrait un coup. Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. En théorie, un CPU à exécution dans le désordre, non-superscalaire, mais avec beaucoup d'unités de calcul, fait face au même problème. En pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant. La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Évidemment, certaines possibilités de contournement sont perdues, mais c'est compensé par un gain en fréquence et un cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée. ===Le banc de registre des processeurs superscalaires larges=== Sur les processeurs superscalaires larges, le banc de registre doit avoir un grand nombre de ports de lectures. Pour alimenter N ALU, il faut typiquement 2 * N ports de lecture et N ports d'écritures. Et quand N devient trop grand, le banc de registre ne suit pas. Ajouter autant de ports impacterait la fréquence du banc de registres et réduirait les performances, voire est totalement impraticable à cause des contraintes thermiques. Les processeur susperscalaires larges doivent donc trouver des solutions. Une première solution est de dupliquer le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres. Une autre solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''. [[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]] Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat. ==La macro-fusion : une optimisation du décodage parallèle== Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes. La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. En théorie, d'autres utilisations de la macro-fusion sont possibles, certaines ont même été [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], mais n'ont pas été implémentées. La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. Une seconde utilisation, proposée sur le jeu d'instruction RISC-V, fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Cette seconde utilisation a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir un pipeline mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intégre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis. La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs... ==Le ''stack engine'' : une optimisation de la pile d'appel== Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière. L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié. ===Le ''stack engine''=== L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle. Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre. L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire. Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''. Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits. ===Les points de synchronisation du delta=== La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué. D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. A noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte. Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème. La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération. ==La micro-fusion et la délamination== La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instruction ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodée en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications. L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage. L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre. Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés. ===Les domaines de fusion=== La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc. J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc. Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté. Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation. Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU. Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type. ===La délamination=== La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement. Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations. ===La micro-fusion des processeurs Intel et AMD=== La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''. Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''. Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout. La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Le parallélisme mémoire | prevText=Le parallélisme mémoire | next=Exemples de microarchitectures CPU : le cas du x86 | nextText=Exemples de microarchitectures CPU : le cas du x86 }} </noinclude> p8u17p5d5czew61xf4rqqsncxhycuuv 772669 772668 2026-09-21T14:00:08Z Mewtow 31375 orthotypo 772669 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 tout premiers processeurs superscalaires pouvaient émettre deux µops en même temps, ce qui portait le nom de '''double émission'''. Mais le nombre a rapidement augmenté et les processeurs modernes 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. ==L'implémentation des processeurs superscalaires== Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre. Intuitivement, émettre et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Il est aussi possible de dupliquer les unités de calcul pour exécuter plusieurs µops en même temps. Mais d'autres circuits sont simplement modifiés. Par exemple, l'unité de chargement n'est pas dupliquée, mais simplement modifiée, idem pour l'unité d'émission et les circuits d'exécution dans le désordre. Voyons ce qu'il en est dans le détail. ===Les unités de calcul d'un processeur superscalaire=== Intuitivement, on se dit que les unités de calcul doivent être dupliquées. Par exemple, un processeur à double émission devrait dupliquer chaque ALU et FPU en deux exemplaires (N exemplaires pour un CPU à N voies). L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission. Le problème, c'est que le cout en circuit est énorme ! Faire cela revient à dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, etc. Un autre cas extrême est celui où aucune unité de calcul n'est dupliquée. Pour comprendre comment c'est possible rappelez-vous qu'un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et éventuellement une unité de branchement. Cela suffit pour créer un processeur quadruple émission, très limité. Un tel processeur peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire. Le problème est qu'il est rare que l'on ait à exécuter en même temps un calcul entier, un calcul flottant, un accès mémoire et un branchement. La quadruple émission n'est donc que rarement utilisée. En pratique, les seules combinaisons fréquentes sont les combinaisons calcul + accès mémoire et calcul + branchement. Les combinaisons calcul entier + calcul flottant sont aussi possibles, mais plus rares. Mais on dépasse rarement la double émission. De plus, au-delà de la quadruple émission, on ne peut plus se contenter d'une ALU, d'une FPU, d'une unité de branchement et d'une unité mémoire. Les processeurs superscalaires larges n'ont pas le choix que de rajouter des unités de calcul, voire des unités mémoire. Reste à voir ce qui est dupliqué en pratique. En pratique, les CPU superscalaires sont un intermédiaire entre ces deux cas extrêmes : certaines unités de calcul sont dupliquées en plusieurs exemplaires, mais pas toutes. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant. La conséquence est que les processeurs superscalaires ont des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a des paires d'instructions autorisées et des paires interdites. Par exemple, l'exécution simultanée de deux branchements est interdite, les branchements sont exécutés l'un après l'autre. Mais il est possible d'émettre un branchement en même temps qu'une autre instruction, en espérant que la prédiction de branchement ait fait une bonne prédiction. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur. En pratique, ces contraintes d’appariement ont peu d’impact. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière, le ratio est le même quand on remplace la multiplication par un branchement ou un accès mémoire. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Deux branchements consécutifs sont quant à eux plus fréquents, idem pour deux accès mémoire. Dupliquer des ALU entières est donc la priorité, suivi par la duplication des unités de branchement, puis celle des accès mémoire, pour terminer par le multiplieur et la FPU. ===Le chargement de plusieurs instructions consécutives=== Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large. Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence. Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas. Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement non-pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc. [[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]] ===Le décodage parallèle des instructions=== Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits. Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur. Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! ===L'unité d'émission d'un processeur superscalaire=== La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. Nous verrons pourquoi dans la suite, mais on peut dire que c'est juste plus simple et plus efficace ainsi. Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop candidate, sous entendu "candidate à l'émission". L'unité d'émission détecte les dépendances entre la µop candidate et les µops en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en cours d'exécution dans le pipeline. mais en plus, elle doit tester les dépendances entre les µops candidates. Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément, il doit détecter les paires d'instructions dépendantes et les sérialiser. Avec une fenêtre d'instruction, il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil gérent les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires modernes utilisent tous une fenêtre d'instruction centralisée ou décentralisée, et non des ''scoreboard''. Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps, là encore en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire peut émettre 8 µops à la fois, on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle. ===L'unité de renommage superscalaire=== Avec la superscalarité, l'unité de renommage de registre doit gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance de registre, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées. Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat. [[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]] Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante. [[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]] Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande. [[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]] ===Le banc de registre d'un processeur superscalaire=== Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement. 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. ===L’impact de la superscalarité sur les autres circuits=== Pour résumer le tout, voici ce que cela donne dans les grandes lignes. * L'unité de chargement charge deux fois plus de données, mais n'est pas modifiée en profondeur. * Les décodeurs sont dupliqués, ce qui a un cout en circuit pas négligeable. * Les circuits d'émission voient leur nombre de ports doubler/tripler/quadrupler. * L'unité de renommage de registre, le ROB et les circuits d’exécution dans le désordre sont légèrement altérés. * Les unités de calcul peuvent être dupliquées, mais ce n'est pas systématique. * Le banc de registre et l'unité mémoire ne sont pas forcément modifiés, mais s'ils le sont, c'est pour leur ajouter des ports. {|class="wikitable" |- ! rowspan="2" | Processeur sans émission multiple | rowspan="2" | Chargement | rowspan="2" | Décodage | rowspan="2" | Renommage | rowspan="2" | Émission | Exécution / ALU | rowspan="2" | ''Commit''/ROB |- | Exécution / ALU |- ! rowspan="4" | Processeur superscalaire | rowspan="4" | Chargement | rowspan="2" | Décodage | rowspan="4" | Renommage | rowspan="4" | Émission | Exécution / ALU | rowspan="4" | ''Commit''/ROB |- | Exécution / ALU |- | rowspan="2" | Décodage | Exécution / ALU |- | Exécution / ALU |} Pour les unités de calcul, tout dépend du processeur. Il existe même des processeurs sur lesquels aucune unité de calcul n'est ajoutée ! Et pour comprendre pourquoi, nous allons parler des processeurs superscalaires historiques. ==L'évolution historique des processeurs superscalaires== Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs triple émission permettent eux d’émettre trois instructions à la fois. Les tout premiers processeurs superscalaires avaient un budget en transistors très limité, ce qui fait qu'ils ne pouvaient pas être "totalement superscalaires". Par exemple, un processeur double émission ne pouvait pas émettre deux opérations entières en même temps, ni deux opérations flottantes, ni une opération entière avec un accès mémoire. Il y avait des paires d'instructions autorisées et des paires interdites. Les '''contraintes d'appariement''' sur les instructions à émettre en même temps étaient très strictes. L'intérêt de ces contraintes d’appariement est que le processeur était très simple, il n'y avait pas besoin de rajouter des circuits complexes pour rendre le processeur superscalaire. Les CPU superscalaires historiques ne faisaient que mieux utiliser des unités de calcul déjà présentes. Prenons un processeur non-superscalaire simple, illustré ci-dessous. Il contient : une unité de chargement, un décodeur, une unité d'émission, une ALU, une FPU, les bancs de registre entier et flottants, et une unité mémoire. Nous avons placé l'unité mémoire à la suite de l'ALU entière, car ce sera important pour les explications qui vont suivre. [[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]] Les processeurs superscalaires historiques modifiaient assez peu le schéma précédent. L'unité de chargement était adaptée pour lire plusieurs instructions depuis la mémoire, il y avait un seul décodeur, l'unité d'émission était assez peu modifiée. Mais une chose était certaine : aucune unité de calcul n'était dupliquée, le banc de registre n'était presque pas modifié. A la rigueur, gérer la triple 'émission avec les branchements demandait parfois d'ajouter des ports de lecture sur le banc de registre généraux, pas plus. ===La double émission entière-flottante=== Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est d'émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Le processeur a donc deux pipelines séparés : un pour les micro-opérations entières, un autre pour les micro-opérations flottantes. On parle alors de '''double émission entière-flottante'''. La mise en œuvre utilise assez peu de circuits, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité. La plupart des premiers processeurs superscalaires étaient de ce type. Les exemples les plus notables sont les processeurs POWER 1 et ses dérivés comme le ''RISC Single Chip''. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle. [[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]] La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisations visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate]. ===L'émission parallèle des branchements=== Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur. L'idée était d'appliquer un équivalent de la double émission, non pas pour des opérations flottantes, mais pour autre chose. Il y avait deux candidats : l'unité de branchement, l'unité mémoire. Et c'est l'unité de branchement qui a été choisie. Le processeur pouvait émettre un branchement en parallèle d'une instruction entière. La technique s'appelle l''''émission parallèle des branchements'''. Combinée à la double émission entière-flottante, cela permettait d'émettre trois µops en même temps : un branchement, une opération flottante, et une opération entière/mémoire. Les exemples de processeurs de ce type ne sont pas très nombreux, mais on peut citer l'Intel i960CA et l'HyperSPARC. Les branchements étant fréquents, surtout sur les CPU de type RISC, les gains en performances étaient assez impressionnants. Quant au cout en transistor, il dépendait du jeu d’instruction, expliquons pourquoi. Pour les processeurs avec un registre d'état, l'implémentation est particulièrement simple. L'unité de branchement lit ce registre d'état, sélectionne le bit adéquat, teste sa valeur, et modifie le ''program counter'' en fonction. Le processeur intègre déjà une unité de branchement séparée de l'unité de calcul, pour faire tout cela. Pas besoin d'utiliser l'unité de calcul entière, ni toute autre unité. Il n'y a même pas besoin de toucher au banc de registres, vu que le registre d'état n'est pas dedans, idem pour le ''program counter'' Et il en est de même si le processeur utiliser des registres à prédicats. Là encore, il y a un banc de registre séparé pour les prédicats et une unité de branchement dédiée. L'implémentation de la triple émission ne fait alors qu'utiliser au mieux des circuits existants. Sur les CPU de type RISC, sans registre d'état, les choses sont cependant différentes. De tels processeurs ont des instructions de branchements qui lisent deux opérandes dans les registres, les comparent, et altèrent ou non le ''program counter'' suivant le résultat. L'implémentation de la triple émission demande alors d'ajouter deux ports de lecture au banc de registre entier, pour lire les opérandes. Il faut aussi ajouter une unité pour faire la comparaison, si elle n'était pas déjà présente. Certains CPU RISC utilisent une unité de branchement séparée, mais d'autres utilisaient l'ALU entière pour. ===L'émission multiple des micro-opérations entières=== Voyons maintenant l'étape après la triple émission entière-flottante-branchement. Elle est assez simple, à savoir qu'elle permet d'émettre deux instructions entières en même temps, avec éventuellement une instruction flottante en plus. Il est parfois possible de monter à trois ou quatre instructions entières. 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 modifier le banc de registres. Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. [[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]] Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications et les divisions ? Pour étudier ces questions, nous allons prendre le cas d'un processeur capable d’émettre trois opérations en même temps : deux opérations entières, et une flottante, potentiellement d'autres. Le cas des opérations complexes est assez intéressant. Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les ALUs en N exemplaires. Mais ce n'est jamais fait en pratique. Concrètement, seules les ALU entières simples le sont, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ne sont pas dupliqués en N exemplaires, le cout en transistors serait trop important. Pour un processeur à double émission, l'unité d'émission a deux ports d'émission, pour émettre deux µops entières. Les deux ports ont chacun leur ALU entière, mais l'un d'entre eux a est aussi relié au circuit multiplier. En clair, cela permet d'émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Idem pour les opérations simples comme les soustractions, les opérations bit à bit, etc. On peut en exécuter deux, ou en coupler une avec une multiplication. Faire ainsi n'a pas de cout en performance significatif, comparé à une duplication totale des ALUs entières. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur serait inutile. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle. [[File:Emission multiple des opérations entières, implémentation naive.png|centre|vignette|upright=2|Émission multiple des opérations entières, implémentation naïve.]] Il en est de même avec les décalages, vu que le ''barrel shifter'' n'est pas dupliqué. Il est donc possible d'émettre deux opérations de base, un décalage avec une opération de base, mais pas deux décalages en parallèle. En général, le ''barrel shifter'' et le multiplieur sont placés sur deux ports d'émission séparés. Cela permet ainsi de faire un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas. [[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Emission multiple des opérations entières, implémentation courante]] Pour les branchements, deux solutions sont possibles. Avec la première, il y a émission parallèle des branchements, à savoir qu'il est possible d'émettre en branchement en parallèle de deux-trois instructions entières. Et cela impose la présence d'une unité de branchement séparée, avec son port d'émission. Avec la seconde, les branchements sont traités comme des instructions entières. Il peut y avoir une unité de branchement séparée, mais ce n'est pas souvent le cas. En général, les branchements sont exécutés dans une ALU entière. [[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Emission série et parallèle des branchements]] Les accès mémoire sont traités comme des opérations entières un peu spéciales, sur les processeurs superscalaires historiques. En clair, le processeur peut émettre deux opérations entières, ou une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent. Les raisons à cela étaient nombreuses. Et ces raisons sont encore valables sur des petits processeurs destinés à l'embarqué ou l'informatique industrielle. Seuls les processeurs haute performance modernes font différemment. La raison principale est le cout en circuit. Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse. Pas besoin de rajouter une unité de calcul d'adresse séparée. Mais surtout, permettre l'émission parallèle des accès mémoire ferait passer un processeur triple émission à de la quadruple émission, avec tous les couts que ça implique. Il faudrait rajouter des ports de lecture/écriture au banc de registre généraux, pour traiter un accès mémoire en plus du reste. Les circuits d'émission et de chargement devraient être élargit, aussi. Et quitte à payer un tel cout en circuit, il serait plus rentable de l'utiliser pour émettre une quatrième opération entière qu'un accès mémoire. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand-chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2. ===Les CPU superscalaires à plusieurs unités spécialisées=== Le classique deux opérations entières + une flottante était l'idéal pour la triple émission. Même les processeurs modernes à triple émission restent sur cette "norme", car elle est proche de l'optimal. Mais arrivé à la quadruple émission, les possibilités sont devenues plus diverses. L'évolution des CPU superscalaires ont évolués dans des directions différentes, chacun faisant à sa sauce, les choses sont devenues plus confuses. Le seul point commun est qu'ils ont eu tendance à rajouter des unités de calcul. Un point important est que la quadruple émission permet de nombreuses possibilités, mais la plupart ne sont pas intéressantes. Pour donner un exemple concret, prenons un CPU superscalaire très simple, comprenant une ALU entière, une FPU, une unité de branchement, et une unité mémoire. Il est possible de créer un CPU quadruple émission avec ces unités, mais le gain en performance n'en vaudrait pas la peine pour le cout en circuit. Il est rare qu'un bloc de 4 instructions contienne exactement une opération entière, une flottante, un branchement et un accès mémoire. En pratique, les seules combinaisons fréquentes sont les combinaisons calcul + accès mémoire et calcul + branchement. Les combinaisons calcul entier + calcul flottant sont aussi possibles, mais plus rares. Mais on dépasse rarement la double émission. Il est préférable d'utiliser ces unités dans un CPU double ou triple émission, en retirant l'unité de branchement et/ou en couplant l'unité mémoire à l'ALU. En clair, une fois arrivé à la quadruple émission, on ne peut plus se contenter d'une ALU, d'une FPU, d'une unité de branchement et d'une unité mémoire. Les processeurs superscalaires larges n'ont pas le choix que de rajouter des unités de calcul, voire des unités mémoire. Et en pratique, ils ont privilégié l'ajout d'ALU entières, avant de dupliquer les autres circuits. La raison est que les programmes informatiques utilisent beaucoup plus d'additions/soustractions/comparaisons que le reste. Il y a environ 5 opérations entières de base pour une multiplication entière, le ratio est le même quand on remplace la multiplication par un branchement ou un accès mémoire. En conséquence, les ALU entières ont été dupliquées en premier, et dans de nombreux exemplaires. Reste à voir comment a été dupliqué le reste. Les multiplieurs n'ont pas été dupliqués, sauf sur les processeurs superscalaires très larges. La raison est qu'il est rare de devoir exécuter deux multiplications à la fois. C'est la même chose pour le ''barrel shifter''. Deux branchements consécutifs sont quant à eux plus fréquents, c'est même assez courant. Aussi, les unités de branchement ont été dupliquées en second lieu, après les ALU entières. Avoir plusieurs accès mémoire proches est assez courant, surtout sur les CPU de type CISC. Aussi, les unités mémoire ont été dupliquées en troisième lieu. Pour résumer, dupliquer des ALU entières est donc la priorité, suivi par la duplication des unités de branchement, puis celle des accès mémoire, pour terminer par le multiplieur. La FPU est un peu à part. Une autre tendance a été de séparer l'unité mémoire des ALU entières, ce qui a permis l''''émission parallèle des µops mémoire'''. Comme son nom l'indique, l'idée est de séparer les µops mémoire des opérations entières. Il devient alors possible d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière. La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple. La '''double émission flottante''' a été utilisée sur les processeurs Alpha. L'idée est d'émettre deux opérations flottantes à la fois. Sauf qu'une implémentation naïve demanderait de dupliquer la FPU, ce qui aurait un cout en circuit rédhibitoire. Pour rappel, les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant. L'idée est d'avoir des ports d'émission séparés pour l'additionneur flottant et le multiplieur flottant. Le processeur peut alors émettre une addition flottante en même temps qu'une multiplication flottante. Les autres circuits de calcul flottant sont répartis sur ces deux ports d'émission flottants. L'avantage est que cela se marie bien avec l'usage d'une fenêtre d'instruction séparée pour les opérations flottantes. La fenêtre d'instruction a alors deux ports séparés, au lieu d'un seul. Rajouter un second port d'émission flottant n'est pas trop un problème, car le cout lié à l'ajout d'un port n'est pas linéaire. Passer de un port à deux a un cout tolérable, bien plus que de passer de 3 ports à 4 ou de 4 à 5. Il est possible de dupliquer l'additionneur et le multiplieur flottant, ce qui permet d'émettre deux additions et multiplications flottantes en même temps. C'est ce qui est fait sur les processeurs AMD d'architecture Zen 1 et 2. Ils ont deux additionneurs flottants par cœur, deux multiplieurs flottants, chacun avec leur propre port d'émission. Les performances en calcul flottant sont assez impressionnantes pour un processeur de l'époque. [[File:ZEN - émission multiple flottante.png|centre|vignette|upright=2|Microarchitecture Zen 1 d'AMD.]] ==Les ports d'émission et de décodage== Les ports d'émission et de décodage sont des ressources matérielles comme les autres. Dans cette section, nous allons parler de concepts liés aux ports d'émission et de décodage. Nous allons notamment parler du partage des ports d'émission, ainsi que des relations numériques entre ces deux types de ports. ===Le partage des ports d'émission et la ''port contention''=== Il est possible de partager un port d'émission, à savoir de connecter un port d'émission à plusieurs unités de calcul. Nous en avons vu un exemple plus haut, avec l'émission multiple des opérations entières. On peut par exemple connecter un multiplieur et une ALU entière sur le même port d'émission. Pour le moment, nous n'avons vu que des cas où un port d'émission est relié soit à des ALU entières, soit à des FPU, pas aux deux. Mais il existe des processeurs où un même port d'émission alimente à la fois une ALU entière et une FPU. Par exemple, on peut relier un additionneur flottant sur le même port qu'une ALU entière. Il faut noter que cela implique une fenêtre d'instruction centralisée, capable de mettre en attente micro-opérations entières et flottantes. Un exemple est celui des processeurs Core 2 Duo. Ils disposent de 6 ports d'émission, dont 3 ports dédiés à l'unité mémoire. Les 3 ports restants alimentent chacun : une ALU entière, un circuit de calcul flottant et une unité de calcul SSE (une unité de calcul SIMD, qu'on abordera dans quelques chapitres). * Le premier port alimente une ALU entière simple et un multiplieur/diviseur flottant. * Le second alimente une ALU entière, un multiplieur entier et un additionneur flottant. * Le troisième alimente une ALU entière, sans circuit flottant dédié. Dans le même genre, il est possible de partager un port d'émission entre l'unité mémoire et une ALU entière. Cela permet d'utiliser l'ALU entière pour les calculs d'adresse, ce qui évite d'avoir à utiliser une unité de calcul d'adresse distincte. Un exemple est celui du processeur superscalaire double émission Power PC 440. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. L'organisation en question permet soit d'émettre un accès mémoire en même temps qu'une opération entière, soit d'émettre deux opérations entières simples, soit d’émettre une multiplication et une addition/soustraction/comparaison. Une organisation simple, mais très efficace ! [[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]] Regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles, qui ne se présentent que pour certaines combinaisons de micro-opérations bien précises. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port. Le nombre de ports peut être un facteur limitant pour la performance dans certaines situations de '''''port contention''''' où on a assez d'ALU pour exécuter N micro-opérations, mais où la répartition des ALUs sur les ports l’empêche. Mais on peut réduire la ''port contention'' en répartissant correctement les ALU sur les ports d'émission. Le choix en question dépendent fortement du fait qu'en moyenne, que certaines paires d'instructions sont plus fréquentes que d'autres. Tout cela guide ce choix de répartition des ALUs sur les ports. Nous venons de voir que la répartition des ALU sur les ports d'émission est très variable d'un processeur à l'autre. Les divers choix possibles sont tous des compromis entre deux forces : réduire le nombre de ports d'émission d'un côté, garder de bonnes performances en réduisant la ''port contention'' de l'autre. Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction relativement simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ? Par contre, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal réparti les ALU. ===La séparation entre largeur de décodage et d'émission=== Sur les processeurs superscalaires larges, le processeur peut émettre plus d'instructions qu'il peut en décoder. Plus haut, j'ai dit que l'unité d'émission avait parfois plus de ports d'émission que de ports de décodage. La raison est assez simple, et elle s'explique assez bien avec un exemple. Prenons un processeur avec trois ALU entières, une FPU, une unité de branchement et une unité mémoire. Chacune a son propre port d'émission, ce qui fait que le CPU est capable d'émettre 6 µops. Du moins, c'est la théorie, car les conditions pour sont assez rares. Il faut qu'en chargeant un paquet de 6 instructions, il y ait exactement trois instructions entières, une flottante, un branchement et un accès mémoire. Si le paquet contient 5 instructions entières et un branchement, seules quatre instructions sont émises. Dans cet exemple, dans la grande majorité des cas, seuls 4 ports seront utilisés en moyenne et deux seront sous-utilisés. Il s'agit là d'une moyenne, qui dépend du code, et des instructions exactes à exécuter. Cependant, cette moyenne nous donne un moyen d'économiser beaucoup de circuits pour un cout en performance mineur. L'idée est de ne charger que des blocs de 4 instructions, mais d'avoir 6 ports d'émission. Le processeur charge et décode 4 instructions, les quatre instructions sont envoyées sur les ports d'émission adéquat. Le cout en transistors est alors situé entre celui d'un CPU quadruple émission et sextuple émission. Et les performances sont aussi entre les deux. Pour les performances, c'est parce que cela permet d'avoir un CPU quadruple émission avec bien moins de contraintes d’appariement. Imaginez qu'on se soit limité à quatre ports d'émission : il aurait fallu mettre plusieurs unités sur le même port d'émission. En conséquence, il y aurait eu des combinaisons interdites, comme émettre deux instructions entières + un branchement + un accès mémoire ; ou deux instructions entières + une opération flottante + un accès mémoire. De tells appariement interdits sont possibles en utilisant les six ports d'émission. ==L'émission multiple des accès mémoire== Les premiers processeurs superscalaires ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles. Par contre, c'est autre chose sur les processeurs superscalaires larges. Vu qu'ils travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares. L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. La ''load/store queue'' reste une structure unique, mais on peut lui rajouter des ports. Et à ce peit jeu, il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire. La première duplique les unités de calcul, mais pas les ports d'accès au cache. Les accès mémoire sont alors accumulés dans la ''load/store queue'', qui sont ensuite traités un par un par le cache. Un processeur utilisant cette technique est, par exemple, capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. Un exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. Ces processeurs pouvaient calculer trois adresses simultanément, mais ne pouvaient faire qu'un seul accès au cache par cycle. Le cout en circuit est modéré : les unités de calcul coutent très peu, les ports de la ''load/store queue'' coutent déjà un peu plus. La seconde méthode ajoute des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultanées. Il faut alors dupliquer des unités de calcul, rajouter les ports sur le cache, mais aussi rajouter des ports sur la ''load/store queue'', pour qu'elle échange des données/commandes mémoire avec le cache. Le cout en circuits est alors conséquent Quelques processeurs utilisaient des solutions intermédiaires, un exemple classique étant les Pentium 1 et 2 d'Intel. Mais nous en parlerons dans le chapitre suivant. [[File:Processeur non-superscalaire basique 02.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]] Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre. Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Le Pentium 4 était dans ce cas, mais il était un peu seul dans sa catégorie. Un cas similaire est celui où l'unité mémoire est alimentée par une fenêtre de micro-opération mélangeant instructions mémoire et entières. Les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. Dans ce cas, l'émission multiple est rarement implémentée. Le cas suivant a une file pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique. Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui pouvaient seulement émettre une lecture en même temps qu'une écriture. Ils avaient deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse. [[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]] Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture demande d'utiliser une file d'écriture multiport, ce qui est assez compliqué. Implémenter des lectures en plus est plus simple : il suffit de rajouter une seconde file de µops LOAD et une unité de calcul, alimentées via un second port d'émission. Il est aussi possible de rajouter un second port de lecture au cache, mais ce n'est pas obligatoire. Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse. De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports de lecture et d'écriture car il y a plus de lecture que d'écritures. Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. : Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse. [[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]] Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettent plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail. Pour résumer, l'évolution des CPU superscalaires a été la suivante : * pipeline mémorie fusionné avec le pipeline entier ; * séparation du pipeline mémoire du reste, avec émission parallèle des accès mémoire ; * émission multiple, mais sans dupliquer les ports du cache ; * émission multiple, avec émission simultanée d'une lecture et d’une écriture dans le cache ; * émission multiple avec plusieurs accès en lecture au cache. ==Les optimisations des processeurs superscalaires larges== Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul. ===L'étape de chargement d'un CPU superscalaire large=== Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 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 autoaligné.]] Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements ! [[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]] Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez 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 microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total). Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois 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 microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement. Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations. ===La micro-fusion des processeurs Intel et AMD=== La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''. Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''. Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout. La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Le parallélisme mémoire | prevText=Le parallélisme mémoire | next=Exemples de microarchitectures CPU : le cas du x86 | nextText=Exemples de microarchitectures CPU : le cas du x86 }} </noinclude> t4gbcpk9bnlk843f2ici8drpc8cdanx 772671 772669 2026-09-21T14:18:23Z Mewtow 31375 /* Les unités de calcul d'un processeur superscalaire */ 772671 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 tout premiers processeurs superscalaires pouvaient émettre deux µops en même temps, ce qui portait le nom de '''double émission'''. Mais le nombre a rapidement augmenté et les processeurs modernes 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. ==L'implémentation des processeurs superscalaires== Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre. Intuitivement, émettre et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Il est aussi possible de dupliquer les unités de calcul pour exécuter plusieurs µops en même temps. Mais d'autres circuits sont simplement modifiés. Par exemple, l'unité de chargement n'est pas dupliquée, mais simplement modifiée, idem pour l'unité d'émission et les circuits d'exécution dans le désordre. Voyons ce qu'il en est dans le détail. ===Les unités de calcul d'un processeur superscalaire=== Émettre N instructions simultanément demande implicitement d'exécuter N instructions par cycle, voire plus si des instructions multicycles viennent mettre leur grain de sel. Intuitivement, on se dit que les unités de calcul doivent être dupliquées. Et c'est une solution viable pour les processeurs très simples, avec seulement quelques ALU entières. Par exemple, un processeur RISC basique avec seulement une ALU entière et un ''barrel shifter'' peut être rendu superscalaire en dupliquant les deux circuits. Le problème, est que cette solution marche assez mal dès que le processeur contient des circuits multiplieurs et/ou une FPU. Par exemple, un processeur à double émission devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, etc. 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 ! Par contre, l'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission. Un autre cas extrême est celui où aucune unité de calcul n'est dupliquée. Pour comprendre comment c'est possible rappelez-vous qu'un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et éventuellement une unité de branchement. Cela suffit pour créer un processeur quadruple émission, très limité. Un tel processeur peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire. Le problème est qu'il est rare que l'on ait à exécuter en même temps un calcul entier, un calcul flottant, un accès mémoire et un branchement. La quadruple émission n'est donc que rarement utilisée. En pratique, les seules combinaisons fréquentes sont les combinaisons calcul + accès mémoire et calcul + branchement. Les combinaisons calcul entier + calcul flottant sont aussi possibles, mais plus rares. Mais on dépasse rarement la double émission. De plus, au-delà de la quadruple émission, on ne peut plus se contenter d'une ALU, d'une FPU, d'une unité de branchement et d'une unité mémoire. Les processeurs superscalaires larges n'ont pas le choix que de rajouter des unités de calcul, voire des unités mémoire. Reste à voir ce qui est dupliqué en pratique. En pratique, les CPU superscalaires sont un intermédiaire entre ces deux cas extrêmes : certaines unités de calcul sont dupliquées en plusieurs exemplaires, mais pas toutes. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant. La conséquence est que les processeurs superscalaires ont des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a des paires d'instructions autorisées et des paires interdites. Par exemple, l'exécution simultanée de deux branchements est interdite, les branchements sont exécutés l'un après l'autre. Mais il est possible d'émettre un branchement en même temps qu'une autre instruction, en espérant que la prédiction de branchement ait fait une bonne prédiction. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur. En pratique, ces contraintes d’appariement ont peu d’impact. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière, le ratio est le même quand on remplace la multiplication par un branchement ou un accès mémoire. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Deux branchements consécutifs sont quant à eux plus fréquents, idem pour deux accès mémoire. Dupliquer des ALU entières est donc la priorité, suivi par la duplication des unités de branchement, puis celle des accès mémoire, pour terminer par le multiplieur et la FPU. ===Le chargement de plusieurs instructions consécutives=== Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large. Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence. Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas. Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement non-pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc. [[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]] ===Le décodage parallèle des instructions=== Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits. Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur. Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! ===L'unité d'émission d'un processeur superscalaire=== La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. Nous verrons pourquoi dans la suite, mais on peut dire que c'est juste plus simple et plus efficace ainsi. Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop candidate, sous entendu "candidate à l'émission". L'unité d'émission détecte les dépendances entre la µop candidate et les µops en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en cours d'exécution dans le pipeline. mais en plus, elle doit tester les dépendances entre les µops candidates. Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément, il doit détecter les paires d'instructions dépendantes et les sérialiser. Avec une fenêtre d'instruction, il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil gérent les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires modernes utilisent tous une fenêtre d'instruction centralisée ou décentralisée, et non des ''scoreboard''. Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps, là encore en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire peut émettre 8 µops à la fois, on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle. ===L'unité de renommage superscalaire=== Avec la superscalarité, l'unité de renommage de registre doit gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance de registre, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées. Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat. [[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]] Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante. [[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]] Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande. [[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]] ===Le banc de registre d'un processeur superscalaire=== Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement. 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. ===L’impact de la superscalarité sur les autres circuits=== Pour résumer le tout, voici ce que cela donne dans les grandes lignes. * L'unité de chargement charge deux fois plus de données, mais n'est pas modifiée en profondeur. * Les décodeurs sont dupliqués, ce qui a un cout en circuit pas négligeable. * Les circuits d'émission voient leur nombre de ports doubler/tripler/quadrupler. * L'unité de renommage de registre, le ROB et les circuits d’exécution dans le désordre sont légèrement altérés. * Les unités de calcul peuvent être dupliquées, mais ce n'est pas systématique. * Le banc de registre et l'unité mémoire ne sont pas forcément modifiés, mais s'ils le sont, c'est pour leur ajouter des ports. {|class="wikitable" |- ! rowspan="2" | Processeur sans émission multiple | rowspan="2" | Chargement | rowspan="2" | Décodage | rowspan="2" | Renommage | rowspan="2" | Émission | Exécution / ALU | rowspan="2" | ''Commit''/ROB |- | Exécution / ALU |- ! rowspan="4" | Processeur superscalaire | rowspan="4" | Chargement | rowspan="2" | Décodage | rowspan="4" | Renommage | rowspan="4" | Émission | Exécution / ALU | rowspan="4" | ''Commit''/ROB |- | Exécution / ALU |- | rowspan="2" | Décodage | Exécution / ALU |- | Exécution / ALU |} Pour les unités de calcul, tout dépend du processeur. Il existe même des processeurs sur lesquels aucune unité de calcul n'est ajoutée ! Et pour comprendre pourquoi, nous allons parler des processeurs superscalaires historiques. ==L'évolution historique des processeurs superscalaires== Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs triple émission permettent eux d’émettre trois instructions à la fois. Les tout premiers processeurs superscalaires avaient un budget en transistors très limité, ce qui fait qu'ils ne pouvaient pas être "totalement superscalaires". Par exemple, un processeur double émission ne pouvait pas émettre deux opérations entières en même temps, ni deux opérations flottantes, ni une opération entière avec un accès mémoire. Il y avait des paires d'instructions autorisées et des paires interdites. Les '''contraintes d'appariement''' sur les instructions à émettre en même temps étaient très strictes. L'intérêt de ces contraintes d’appariement est que le processeur était très simple, il n'y avait pas besoin de rajouter des circuits complexes pour rendre le processeur superscalaire. Les CPU superscalaires historiques ne faisaient que mieux utiliser des unités de calcul déjà présentes. Prenons un processeur non-superscalaire simple, illustré ci-dessous. Il contient : une unité de chargement, un décodeur, une unité d'émission, une ALU, une FPU, les bancs de registre entier et flottants, et une unité mémoire. Nous avons placé l'unité mémoire à la suite de l'ALU entière, car ce sera important pour les explications qui vont suivre. [[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]] Les processeurs superscalaires historiques modifiaient assez peu le schéma précédent. L'unité de chargement était adaptée pour lire plusieurs instructions depuis la mémoire, il y avait un seul décodeur, l'unité d'émission était assez peu modifiée. Mais une chose était certaine : aucune unité de calcul n'était dupliquée, le banc de registre n'était presque pas modifié. A la rigueur, gérer la triple 'émission avec les branchements demandait parfois d'ajouter des ports de lecture sur le banc de registre généraux, pas plus. ===La double émission entière-flottante=== Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est d'émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Le processeur a donc deux pipelines séparés : un pour les micro-opérations entières, un autre pour les micro-opérations flottantes. On parle alors de '''double émission entière-flottante'''. La mise en œuvre utilise assez peu de circuits, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité. La plupart des premiers processeurs superscalaires étaient de ce type. Les exemples les plus notables sont les processeurs POWER 1 et ses dérivés comme le ''RISC Single Chip''. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle. [[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]] La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisations visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate]. ===L'émission parallèle des branchements=== Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur. L'idée était d'appliquer un équivalent de la double émission, non pas pour des opérations flottantes, mais pour autre chose. Il y avait deux candidats : l'unité de branchement, l'unité mémoire. Et c'est l'unité de branchement qui a été choisie. Le processeur pouvait émettre un branchement en parallèle d'une instruction entière. La technique s'appelle l''''émission parallèle des branchements'''. Combinée à la double émission entière-flottante, cela permettait d'émettre trois µops en même temps : un branchement, une opération flottante, et une opération entière/mémoire. Les exemples de processeurs de ce type ne sont pas très nombreux, mais on peut citer l'Intel i960CA et l'HyperSPARC. Les branchements étant fréquents, surtout sur les CPU de type RISC, les gains en performances étaient assez impressionnants. Quant au cout en transistor, il dépendait du jeu d’instruction, expliquons pourquoi. Pour les processeurs avec un registre d'état, l'implémentation est particulièrement simple. L'unité de branchement lit ce registre d'état, sélectionne le bit adéquat, teste sa valeur, et modifie le ''program counter'' en fonction. Le processeur intègre déjà une unité de branchement séparée de l'unité de calcul, pour faire tout cela. Pas besoin d'utiliser l'unité de calcul entière, ni toute autre unité. Il n'y a même pas besoin de toucher au banc de registres, vu que le registre d'état n'est pas dedans, idem pour le ''program counter'' Et il en est de même si le processeur utiliser des registres à prédicats. Là encore, il y a un banc de registre séparé pour les prédicats et une unité de branchement dédiée. L'implémentation de la triple émission ne fait alors qu'utiliser au mieux des circuits existants. Sur les CPU de type RISC, sans registre d'état, les choses sont cependant différentes. De tels processeurs ont des instructions de branchements qui lisent deux opérandes dans les registres, les comparent, et altèrent ou non le ''program counter'' suivant le résultat. L'implémentation de la triple émission demande alors d'ajouter deux ports de lecture au banc de registre entier, pour lire les opérandes. Il faut aussi ajouter une unité pour faire la comparaison, si elle n'était pas déjà présente. Certains CPU RISC utilisent une unité de branchement séparée, mais d'autres utilisaient l'ALU entière pour. ===L'émission multiple des micro-opérations entières=== Voyons maintenant l'étape après la triple émission entière-flottante-branchement. Elle est assez simple, à savoir qu'elle permet d'émettre deux instructions entières en même temps, avec éventuellement une instruction flottante en plus. Il est parfois possible de monter à trois ou quatre instructions entières. 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 modifier le banc de registres. Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. [[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]] Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications et les divisions ? Pour étudier ces questions, nous allons prendre le cas d'un processeur capable d’émettre trois opérations en même temps : deux opérations entières, et une flottante, potentiellement d'autres. Le cas des opérations complexes est assez intéressant. Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les ALUs en N exemplaires. Mais ce n'est jamais fait en pratique. Concrètement, seules les ALU entières simples le sont, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ne sont pas dupliqués en N exemplaires, le cout en transistors serait trop important. Pour un processeur à double émission, l'unité d'émission a deux ports d'émission, pour émettre deux µops entières. Les deux ports ont chacun leur ALU entière, mais l'un d'entre eux a est aussi relié au circuit multiplier. En clair, cela permet d'émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Idem pour les opérations simples comme les soustractions, les opérations bit à bit, etc. On peut en exécuter deux, ou en coupler une avec une multiplication. Faire ainsi n'a pas de cout en performance significatif, comparé à une duplication totale des ALUs entières. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur serait inutile. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle. [[File:Emission multiple des opérations entières, implémentation naive.png|centre|vignette|upright=2|Émission multiple des opérations entières, implémentation naïve.]] Il en est de même avec les décalages, vu que le ''barrel shifter'' n'est pas dupliqué. Il est donc possible d'émettre deux opérations de base, un décalage avec une opération de base, mais pas deux décalages en parallèle. En général, le ''barrel shifter'' et le multiplieur sont placés sur deux ports d'émission séparés. Cela permet ainsi de faire un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas. [[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Emission multiple des opérations entières, implémentation courante]] Pour les branchements, deux solutions sont possibles. Avec la première, il y a émission parallèle des branchements, à savoir qu'il est possible d'émettre en branchement en parallèle de deux-trois instructions entières. Et cela impose la présence d'une unité de branchement séparée, avec son port d'émission. Avec la seconde, les branchements sont traités comme des instructions entières. Il peut y avoir une unité de branchement séparée, mais ce n'est pas souvent le cas. En général, les branchements sont exécutés dans une ALU entière. [[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Emission série et parallèle des branchements]] Les accès mémoire sont traités comme des opérations entières un peu spéciales, sur les processeurs superscalaires historiques. En clair, le processeur peut émettre deux opérations entières, ou une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent. Les raisons à cela étaient nombreuses. Et ces raisons sont encore valables sur des petits processeurs destinés à l'embarqué ou l'informatique industrielle. Seuls les processeurs haute performance modernes font différemment. La raison principale est le cout en circuit. Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse. Pas besoin de rajouter une unité de calcul d'adresse séparée. Mais surtout, permettre l'émission parallèle des accès mémoire ferait passer un processeur triple émission à de la quadruple émission, avec tous les couts que ça implique. Il faudrait rajouter des ports de lecture/écriture au banc de registre généraux, pour traiter un accès mémoire en plus du reste. Les circuits d'émission et de chargement devraient être élargit, aussi. Et quitte à payer un tel cout en circuit, il serait plus rentable de l'utiliser pour émettre une quatrième opération entière qu'un accès mémoire. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand-chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2. ===Les CPU superscalaires à plusieurs unités spécialisées=== Le classique deux opérations entières + une flottante était l'idéal pour la triple émission. Même les processeurs modernes à triple émission restent sur cette "norme", car elle est proche de l'optimal. Mais arrivé à la quadruple émission, les possibilités sont devenues plus diverses. L'évolution des CPU superscalaires ont évolués dans des directions différentes, chacun faisant à sa sauce, les choses sont devenues plus confuses. Le seul point commun est qu'ils ont eu tendance à rajouter des unités de calcul. Un point important est que la quadruple émission permet de nombreuses possibilités, mais la plupart ne sont pas intéressantes. Pour donner un exemple concret, prenons un CPU superscalaire très simple, comprenant une ALU entière, une FPU, une unité de branchement, et une unité mémoire. Il est possible de créer un CPU quadruple émission avec ces unités, mais le gain en performance n'en vaudrait pas la peine pour le cout en circuit. Il est rare qu'un bloc de 4 instructions contienne exactement une opération entière, une flottante, un branchement et un accès mémoire. En pratique, les seules combinaisons fréquentes sont les combinaisons calcul + accès mémoire et calcul + branchement. Les combinaisons calcul entier + calcul flottant sont aussi possibles, mais plus rares. Mais on dépasse rarement la double émission. Il est préférable d'utiliser ces unités dans un CPU double ou triple émission, en retirant l'unité de branchement et/ou en couplant l'unité mémoire à l'ALU. En clair, une fois arrivé à la quadruple émission, on ne peut plus se contenter d'une ALU, d'une FPU, d'une unité de branchement et d'une unité mémoire. Les processeurs superscalaires larges n'ont pas le choix que de rajouter des unités de calcul, voire des unités mémoire. Et en pratique, ils ont privilégié l'ajout d'ALU entières, avant de dupliquer les autres circuits. La raison est que les programmes informatiques utilisent beaucoup plus d'additions/soustractions/comparaisons que le reste. Il y a environ 5 opérations entières de base pour une multiplication entière, le ratio est le même quand on remplace la multiplication par un branchement ou un accès mémoire. En conséquence, les ALU entières ont été dupliquées en premier, et dans de nombreux exemplaires. Reste à voir comment a été dupliqué le reste. Les multiplieurs n'ont pas été dupliqués, sauf sur les processeurs superscalaires très larges. La raison est qu'il est rare de devoir exécuter deux multiplications à la fois. C'est la même chose pour le ''barrel shifter''. Deux branchements consécutifs sont quant à eux plus fréquents, c'est même assez courant. Aussi, les unités de branchement ont été dupliquées en second lieu, après les ALU entières. Avoir plusieurs accès mémoire proches est assez courant, surtout sur les CPU de type CISC. Aussi, les unités mémoire ont été dupliquées en troisième lieu. Pour résumer, dupliquer des ALU entières est donc la priorité, suivi par la duplication des unités de branchement, puis celle des accès mémoire, pour terminer par le multiplieur. La FPU est un peu à part. Une autre tendance a été de séparer l'unité mémoire des ALU entières, ce qui a permis l''''émission parallèle des µops mémoire'''. Comme son nom l'indique, l'idée est de séparer les µops mémoire des opérations entières. Il devient alors possible d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière. La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple. La '''double émission flottante''' a été utilisée sur les processeurs Alpha. L'idée est d'émettre deux opérations flottantes à la fois. Sauf qu'une implémentation naïve demanderait de dupliquer la FPU, ce qui aurait un cout en circuit rédhibitoire. Pour rappel, les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant. L'idée est d'avoir des ports d'émission séparés pour l'additionneur flottant et le multiplieur flottant. Le processeur peut alors émettre une addition flottante en même temps qu'une multiplication flottante. Les autres circuits de calcul flottant sont répartis sur ces deux ports d'émission flottants. L'avantage est que cela se marie bien avec l'usage d'une fenêtre d'instruction séparée pour les opérations flottantes. La fenêtre d'instruction a alors deux ports séparés, au lieu d'un seul. Rajouter un second port d'émission flottant n'est pas trop un problème, car le cout lié à l'ajout d'un port n'est pas linéaire. Passer de un port à deux a un cout tolérable, bien plus que de passer de 3 ports à 4 ou de 4 à 5. Il est possible de dupliquer l'additionneur et le multiplieur flottant, ce qui permet d'émettre deux additions et multiplications flottantes en même temps. C'est ce qui est fait sur les processeurs AMD d'architecture Zen 1 et 2. Ils ont deux additionneurs flottants par cœur, deux multiplieurs flottants, chacun avec leur propre port d'émission. Les performances en calcul flottant sont assez impressionnantes pour un processeur de l'époque. [[File:ZEN - émission multiple flottante.png|centre|vignette|upright=2|Microarchitecture Zen 1 d'AMD.]] ==Les ports d'émission et de décodage== Les ports d'émission et de décodage sont des ressources matérielles comme les autres. Dans cette section, nous allons parler de concepts liés aux ports d'émission et de décodage. Nous allons notamment parler du partage des ports d'émission, ainsi que des relations numériques entre ces deux types de ports. ===Le partage des ports d'émission et la ''port contention''=== Il est possible de partager un port d'émission, à savoir de connecter un port d'émission à plusieurs unités de calcul. Nous en avons vu un exemple plus haut, avec l'émission multiple des opérations entières. On peut par exemple connecter un multiplieur et une ALU entière sur le même port d'émission. Pour le moment, nous n'avons vu que des cas où un port d'émission est relié soit à des ALU entières, soit à des FPU, pas aux deux. Mais il existe des processeurs où un même port d'émission alimente à la fois une ALU entière et une FPU. Par exemple, on peut relier un additionneur flottant sur le même port qu'une ALU entière. Il faut noter que cela implique une fenêtre d'instruction centralisée, capable de mettre en attente micro-opérations entières et flottantes. Un exemple est celui des processeurs Core 2 Duo. Ils disposent de 6 ports d'émission, dont 3 ports dédiés à l'unité mémoire. Les 3 ports restants alimentent chacun : une ALU entière, un circuit de calcul flottant et une unité de calcul SSE (une unité de calcul SIMD, qu'on abordera dans quelques chapitres). * Le premier port alimente une ALU entière simple et un multiplieur/diviseur flottant. * Le second alimente une ALU entière, un multiplieur entier et un additionneur flottant. * Le troisième alimente une ALU entière, sans circuit flottant dédié. Dans le même genre, il est possible de partager un port d'émission entre l'unité mémoire et une ALU entière. Cela permet d'utiliser l'ALU entière pour les calculs d'adresse, ce qui évite d'avoir à utiliser une unité de calcul d'adresse distincte. Un exemple est celui du processeur superscalaire double émission Power PC 440. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. L'organisation en question permet soit d'émettre un accès mémoire en même temps qu'une opération entière, soit d'émettre deux opérations entières simples, soit d’émettre une multiplication et une addition/soustraction/comparaison. Une organisation simple, mais très efficace ! [[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]] Regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles, qui ne se présentent que pour certaines combinaisons de micro-opérations bien précises. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port. Le nombre de ports peut être un facteur limitant pour la performance dans certaines situations de '''''port contention''''' où on a assez d'ALU pour exécuter N micro-opérations, mais où la répartition des ALUs sur les ports l’empêche. Mais on peut réduire la ''port contention'' en répartissant correctement les ALU sur les ports d'émission. Le choix en question dépendent fortement du fait qu'en moyenne, que certaines paires d'instructions sont plus fréquentes que d'autres. Tout cela guide ce choix de répartition des ALUs sur les ports. Nous venons de voir que la répartition des ALU sur les ports d'émission est très variable d'un processeur à l'autre. Les divers choix possibles sont tous des compromis entre deux forces : réduire le nombre de ports d'émission d'un côté, garder de bonnes performances en réduisant la ''port contention'' de l'autre. Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction relativement simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ? Par contre, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal réparti les ALU. ===La séparation entre largeur de décodage et d'émission=== Sur les processeurs superscalaires larges, le processeur peut émettre plus d'instructions qu'il peut en décoder. Plus haut, j'ai dit que l'unité d'émission avait parfois plus de ports d'émission que de ports de décodage. La raison est assez simple, et elle s'explique assez bien avec un exemple. Prenons un processeur avec trois ALU entières, une FPU, une unité de branchement et une unité mémoire. Chacune a son propre port d'émission, ce qui fait que le CPU est capable d'émettre 6 µops. Du moins, c'est la théorie, car les conditions pour sont assez rares. Il faut qu'en chargeant un paquet de 6 instructions, il y ait exactement trois instructions entières, une flottante, un branchement et un accès mémoire. Si le paquet contient 5 instructions entières et un branchement, seules quatre instructions sont émises. Dans cet exemple, dans la grande majorité des cas, seuls 4 ports seront utilisés en moyenne et deux seront sous-utilisés. Il s'agit là d'une moyenne, qui dépend du code, et des instructions exactes à exécuter. Cependant, cette moyenne nous donne un moyen d'économiser beaucoup de circuits pour un cout en performance mineur. L'idée est de ne charger que des blocs de 4 instructions, mais d'avoir 6 ports d'émission. Le processeur charge et décode 4 instructions, les quatre instructions sont envoyées sur les ports d'émission adéquat. Le cout en transistors est alors situé entre celui d'un CPU quadruple émission et sextuple émission. Et les performances sont aussi entre les deux. Pour les performances, c'est parce que cela permet d'avoir un CPU quadruple émission avec bien moins de contraintes d’appariement. Imaginez qu'on se soit limité à quatre ports d'émission : il aurait fallu mettre plusieurs unités sur le même port d'émission. En conséquence, il y aurait eu des combinaisons interdites, comme émettre deux instructions entières + un branchement + un accès mémoire ; ou deux instructions entières + une opération flottante + un accès mémoire. De tells appariement interdits sont possibles en utilisant les six ports d'émission. ==L'émission multiple des accès mémoire== Les premiers processeurs superscalaires ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles. Par contre, c'est autre chose sur les processeurs superscalaires larges. Vu qu'ils travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares. L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. La ''load/store queue'' reste une structure unique, mais on peut lui rajouter des ports. Et à ce peit jeu, il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire. La première duplique les unités de calcul, mais pas les ports d'accès au cache. Les accès mémoire sont alors accumulés dans la ''load/store queue'', qui sont ensuite traités un par un par le cache. Un processeur utilisant cette technique est, par exemple, capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. Un exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. Ces processeurs pouvaient calculer trois adresses simultanément, mais ne pouvaient faire qu'un seul accès au cache par cycle. Le cout en circuit est modéré : les unités de calcul coutent très peu, les ports de la ''load/store queue'' coutent déjà un peu plus. La seconde méthode ajoute des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultanées. Il faut alors dupliquer des unités de calcul, rajouter les ports sur le cache, mais aussi rajouter des ports sur la ''load/store queue'', pour qu'elle échange des données/commandes mémoire avec le cache. Le cout en circuits est alors conséquent Quelques processeurs utilisaient des solutions intermédiaires, un exemple classique étant les Pentium 1 et 2 d'Intel. Mais nous en parlerons dans le chapitre suivant. [[File:Processeur non-superscalaire basique 02.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]] Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre. Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Le Pentium 4 était dans ce cas, mais il était un peu seul dans sa catégorie. Un cas similaire est celui où l'unité mémoire est alimentée par une fenêtre de micro-opération mélangeant instructions mémoire et entières. Les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. Dans ce cas, l'émission multiple est rarement implémentée. Le cas suivant a une file pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique. Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui pouvaient seulement émettre une lecture en même temps qu'une écriture. Ils avaient deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse. [[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]] Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture demande d'utiliser une file d'écriture multiport, ce qui est assez compliqué. Implémenter des lectures en plus est plus simple : il suffit de rajouter une seconde file de µops LOAD et une unité de calcul, alimentées via un second port d'émission. Il est aussi possible de rajouter un second port de lecture au cache, mais ce n'est pas obligatoire. Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse. De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports de lecture et d'écriture car il y a plus de lecture que d'écritures. Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. : Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse. [[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]] Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettent plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail. Pour résumer, l'évolution des CPU superscalaires a été la suivante : * pipeline mémorie fusionné avec le pipeline entier ; * séparation du pipeline mémoire du reste, avec émission parallèle des accès mémoire ; * émission multiple, mais sans dupliquer les ports du cache ; * émission multiple, avec émission simultanée d'une lecture et d’une écriture dans le cache ; * émission multiple avec plusieurs accès en lecture au cache. ==Les optimisations des processeurs superscalaires larges== Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul. ===L'étape de chargement d'un CPU superscalaire large=== Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 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 autoaligné.]] Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements ! [[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]] Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez 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 microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total). Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois 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 microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement. Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations. ===La micro-fusion des processeurs Intel et AMD=== La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''. Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''. Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout. La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Le parallélisme mémoire | prevText=Le parallélisme mémoire | next=Exemples de microarchitectures CPU : le cas du x86 | nextText=Exemples de microarchitectures CPU : le cas du x86 }} </noinclude> 5dsir746srzpt6lezjber75w2cdshwr 772672 772671 2026-09-21T14:19:59Z Mewtow 31375 /* Les unités de calcul d'un processeur superscalaire */ 772672 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 tout premiers processeurs superscalaires pouvaient émettre deux µops en même temps, ce qui portait le nom de '''double émission'''. Mais le nombre a rapidement augmenté et les processeurs modernes 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. ==L'implémentation des processeurs superscalaires== Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre. Intuitivement, émettre et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Il est aussi possible de dupliquer les unités de calcul pour exécuter plusieurs µops en même temps. Mais d'autres circuits sont simplement modifiés. Par exemple, l'unité de chargement n'est pas dupliquée, mais simplement modifiée, idem pour l'unité d'émission et les circuits d'exécution dans le désordre. Voyons ce qu'il en est dans le détail. ===Les unités de calcul d'un processeur superscalaire=== Émettre N instructions simultanément demande implicitement d'exécuter N instructions par cycle, voire plus si des instructions multicycles viennent mettre leur grain de sel. Intuitivement, on se dit que les unités de calcul doivent être dupliquées. Et c'est une solution viable pour les processeurs très simples, avec seulement quelques ALU entières. Par exemple, un processeur RISC basique avec seulement une ALU entière et un ''barrel shifter'' peut être rendu superscalaire en dupliquant les deux circuits. Le problème, est que cette solution marche assez mal dès que le processeur contient des circuits multiplieurs et/ou une FPU. Par exemple, un processeur à double émission devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, etc. 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 ! Par contre, l'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission. Un autre cas extrême est celui où aucune unité de calcul n'est dupliquée. Pour comprendre comment c'est possible rappelez-vous qu'un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et éventuellement une unité de branchement. Cela suffit pour créer un processeur quadruple émission, très limité. Un tel processeur peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire. Le problème est qu'il est rare que l'on ait à exécuter en même temps un calcul entier, un calcul flottant, un accès mémoire et un branchement. La quadruple émission n'est donc que rarement utilisée. En pratique, les seules combinaisons fréquentes sont les combinaisons calcul + accès mémoire et calcul + branchement. Les combinaisons calcul entier + calcul flottant sont aussi possibles, mais plus rares. Mais on dépasse rarement la double émission. En clair, à partir de la quadruple émission, on ne peut plus se contenter d'une ALU, d'une FPU, d'une unité de branchement et d'une unité mémoire. Les processeurs superscalaires larges n'ont pas le choix que de rajouter des unités de calcul. Reste à voir ce qui est dupliqué en pratique. 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. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant. La conséquence est que les processeurs superscalaires ont des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a des paires d'instructions autorisées et des paires interdites. Par exemple, l'exécution simultanée de deux branchements est interdite, les branchements sont exécutés l'un après l'autre. Mais il est possible d'émettre un branchement en même temps qu'une autre instruction, en espérant que la prédiction de branchement ait fait une bonne prédiction. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur. En pratique, ces contraintes d’appariement ont peu d’impact. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière, le ratio est le même quand on remplace la multiplication par un branchement ou un accès mémoire. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Deux branchements consécutifs sont quant à eux plus fréquents, idem pour deux accès mémoire. Dupliquer des ALU entières est donc la priorité, suivi par la duplication des unités de branchement, puis celle des accès mémoire, pour terminer par le multiplieur et la FPU. ===Le chargement de plusieurs instructions consécutives=== Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large. Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence. Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas. Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement non-pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc. [[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]] ===Le décodage parallèle des instructions=== Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits. Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur. Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! ===L'unité d'émission d'un processeur superscalaire=== La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. Nous verrons pourquoi dans la suite, mais on peut dire que c'est juste plus simple et plus efficace ainsi. Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop candidate, sous entendu "candidate à l'émission". L'unité d'émission détecte les dépendances entre la µop candidate et les µops en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en cours d'exécution dans le pipeline. mais en plus, elle doit tester les dépendances entre les µops candidates. Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément, il doit détecter les paires d'instructions dépendantes et les sérialiser. Avec une fenêtre d'instruction, il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil gérent les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires modernes utilisent tous une fenêtre d'instruction centralisée ou décentralisée, et non des ''scoreboard''. Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps, là encore en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire peut émettre 8 µops à la fois, on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle. ===L'unité de renommage superscalaire=== Avec la superscalarité, l'unité de renommage de registre doit gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance de registre, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées. Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat. [[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]] Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante. [[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]] Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande. [[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]] ===Le banc de registre d'un processeur superscalaire=== Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement. 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. ===L’impact de la superscalarité sur les autres circuits=== Pour résumer le tout, voici ce que cela donne dans les grandes lignes. * L'unité de chargement charge deux fois plus de données, mais n'est pas modifiée en profondeur. * Les décodeurs sont dupliqués, ce qui a un cout en circuit pas négligeable. * Les circuits d'émission voient leur nombre de ports doubler/tripler/quadrupler. * L'unité de renommage de registre, le ROB et les circuits d’exécution dans le désordre sont légèrement altérés. * Les unités de calcul peuvent être dupliquées, mais ce n'est pas systématique. * Le banc de registre et l'unité mémoire ne sont pas forcément modifiés, mais s'ils le sont, c'est pour leur ajouter des ports. {|class="wikitable" |- ! rowspan="2" | Processeur sans émission multiple | rowspan="2" | Chargement | rowspan="2" | Décodage | rowspan="2" | Renommage | rowspan="2" | Émission | Exécution / ALU | rowspan="2" | ''Commit''/ROB |- | Exécution / ALU |- ! rowspan="4" | Processeur superscalaire | rowspan="4" | Chargement | rowspan="2" | Décodage | rowspan="4" | Renommage | rowspan="4" | Émission | Exécution / ALU | rowspan="4" | ''Commit''/ROB |- | Exécution / ALU |- | rowspan="2" | Décodage | Exécution / ALU |- | Exécution / ALU |} Pour les unités de calcul, tout dépend du processeur. Il existe même des processeurs sur lesquels aucune unité de calcul n'est ajoutée ! Et pour comprendre pourquoi, nous allons parler des processeurs superscalaires historiques. ==L'évolution historique des processeurs superscalaires== Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs triple émission permettent eux d’émettre trois instructions à la fois. Les tout premiers processeurs superscalaires avaient un budget en transistors très limité, ce qui fait qu'ils ne pouvaient pas être "totalement superscalaires". Par exemple, un processeur double émission ne pouvait pas émettre deux opérations entières en même temps, ni deux opérations flottantes, ni une opération entière avec un accès mémoire. Il y avait des paires d'instructions autorisées et des paires interdites. Les '''contraintes d'appariement''' sur les instructions à émettre en même temps étaient très strictes. L'intérêt de ces contraintes d’appariement est que le processeur était très simple, il n'y avait pas besoin de rajouter des circuits complexes pour rendre le processeur superscalaire. Les CPU superscalaires historiques ne faisaient que mieux utiliser des unités de calcul déjà présentes. Prenons un processeur non-superscalaire simple, illustré ci-dessous. Il contient : une unité de chargement, un décodeur, une unité d'émission, une ALU, une FPU, les bancs de registre entier et flottants, et une unité mémoire. Nous avons placé l'unité mémoire à la suite de l'ALU entière, car ce sera important pour les explications qui vont suivre. [[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]] Les processeurs superscalaires historiques modifiaient assez peu le schéma précédent. L'unité de chargement était adaptée pour lire plusieurs instructions depuis la mémoire, il y avait un seul décodeur, l'unité d'émission était assez peu modifiée. Mais une chose était certaine : aucune unité de calcul n'était dupliquée, le banc de registre n'était presque pas modifié. A la rigueur, gérer la triple 'émission avec les branchements demandait parfois d'ajouter des ports de lecture sur le banc de registre généraux, pas plus. ===La double émission entière-flottante=== Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est d'émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Le processeur a donc deux pipelines séparés : un pour les micro-opérations entières, un autre pour les micro-opérations flottantes. On parle alors de '''double émission entière-flottante'''. La mise en œuvre utilise assez peu de circuits, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité. La plupart des premiers processeurs superscalaires étaient de ce type. Les exemples les plus notables sont les processeurs POWER 1 et ses dérivés comme le ''RISC Single Chip''. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle. [[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]] La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisations visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate]. ===L'émission parallèle des branchements=== Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur. L'idée était d'appliquer un équivalent de la double émission, non pas pour des opérations flottantes, mais pour autre chose. Il y avait deux candidats : l'unité de branchement, l'unité mémoire. Et c'est l'unité de branchement qui a été choisie. Le processeur pouvait émettre un branchement en parallèle d'une instruction entière. La technique s'appelle l''''émission parallèle des branchements'''. Combinée à la double émission entière-flottante, cela permettait d'émettre trois µops en même temps : un branchement, une opération flottante, et une opération entière/mémoire. Les exemples de processeurs de ce type ne sont pas très nombreux, mais on peut citer l'Intel i960CA et l'HyperSPARC. Les branchements étant fréquents, surtout sur les CPU de type RISC, les gains en performances étaient assez impressionnants. Quant au cout en transistor, il dépendait du jeu d’instruction, expliquons pourquoi. Pour les processeurs avec un registre d'état, l'implémentation est particulièrement simple. L'unité de branchement lit ce registre d'état, sélectionne le bit adéquat, teste sa valeur, et modifie le ''program counter'' en fonction. Le processeur intègre déjà une unité de branchement séparée de l'unité de calcul, pour faire tout cela. Pas besoin d'utiliser l'unité de calcul entière, ni toute autre unité. Il n'y a même pas besoin de toucher au banc de registres, vu que le registre d'état n'est pas dedans, idem pour le ''program counter'' Et il en est de même si le processeur utiliser des registres à prédicats. Là encore, il y a un banc de registre séparé pour les prédicats et une unité de branchement dédiée. L'implémentation de la triple émission ne fait alors qu'utiliser au mieux des circuits existants. Sur les CPU de type RISC, sans registre d'état, les choses sont cependant différentes. De tels processeurs ont des instructions de branchements qui lisent deux opérandes dans les registres, les comparent, et altèrent ou non le ''program counter'' suivant le résultat. L'implémentation de la triple émission demande alors d'ajouter deux ports de lecture au banc de registre entier, pour lire les opérandes. Il faut aussi ajouter une unité pour faire la comparaison, si elle n'était pas déjà présente. Certains CPU RISC utilisent une unité de branchement séparée, mais d'autres utilisaient l'ALU entière pour. ===L'émission multiple des micro-opérations entières=== Voyons maintenant l'étape après la triple émission entière-flottante-branchement. Elle est assez simple, à savoir qu'elle permet d'émettre deux instructions entières en même temps, avec éventuellement une instruction flottante en plus. Il est parfois possible de monter à trois ou quatre instructions entières. 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 modifier le banc de registres. Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. [[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]] Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications et les divisions ? Pour étudier ces questions, nous allons prendre le cas d'un processeur capable d’émettre trois opérations en même temps : deux opérations entières, et une flottante, potentiellement d'autres. Le cas des opérations complexes est assez intéressant. Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les ALUs en N exemplaires. Mais ce n'est jamais fait en pratique. Concrètement, seules les ALU entières simples le sont, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ne sont pas dupliqués en N exemplaires, le cout en transistors serait trop important. Pour un processeur à double émission, l'unité d'émission a deux ports d'émission, pour émettre deux µops entières. Les deux ports ont chacun leur ALU entière, mais l'un d'entre eux a est aussi relié au circuit multiplier. En clair, cela permet d'émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Idem pour les opérations simples comme les soustractions, les opérations bit à bit, etc. On peut en exécuter deux, ou en coupler une avec une multiplication. Faire ainsi n'a pas de cout en performance significatif, comparé à une duplication totale des ALUs entières. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur serait inutile. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle. [[File:Emission multiple des opérations entières, implémentation naive.png|centre|vignette|upright=2|Émission multiple des opérations entières, implémentation naïve.]] Il en est de même avec les décalages, vu que le ''barrel shifter'' n'est pas dupliqué. Il est donc possible d'émettre deux opérations de base, un décalage avec une opération de base, mais pas deux décalages en parallèle. En général, le ''barrel shifter'' et le multiplieur sont placés sur deux ports d'émission séparés. Cela permet ainsi de faire un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas. [[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Emission multiple des opérations entières, implémentation courante]] Pour les branchements, deux solutions sont possibles. Avec la première, il y a émission parallèle des branchements, à savoir qu'il est possible d'émettre en branchement en parallèle de deux-trois instructions entières. Et cela impose la présence d'une unité de branchement séparée, avec son port d'émission. Avec la seconde, les branchements sont traités comme des instructions entières. Il peut y avoir une unité de branchement séparée, mais ce n'est pas souvent le cas. En général, les branchements sont exécutés dans une ALU entière. [[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Emission série et parallèle des branchements]] Les accès mémoire sont traités comme des opérations entières un peu spéciales, sur les processeurs superscalaires historiques. En clair, le processeur peut émettre deux opérations entières, ou une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent. Les raisons à cela étaient nombreuses. Et ces raisons sont encore valables sur des petits processeurs destinés à l'embarqué ou l'informatique industrielle. Seuls les processeurs haute performance modernes font différemment. La raison principale est le cout en circuit. Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse. Pas besoin de rajouter une unité de calcul d'adresse séparée. Mais surtout, permettre l'émission parallèle des accès mémoire ferait passer un processeur triple émission à de la quadruple émission, avec tous les couts que ça implique. Il faudrait rajouter des ports de lecture/écriture au banc de registre généraux, pour traiter un accès mémoire en plus du reste. Les circuits d'émission et de chargement devraient être élargit, aussi. Et quitte à payer un tel cout en circuit, il serait plus rentable de l'utiliser pour émettre une quatrième opération entière qu'un accès mémoire. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand-chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2. ===Les CPU superscalaires à plusieurs unités spécialisées=== Le classique deux opérations entières + une flottante était l'idéal pour la triple émission. Même les processeurs modernes à triple émission restent sur cette "norme", car elle est proche de l'optimal. Mais arrivé à la quadruple émission, les possibilités sont devenues plus diverses. L'évolution des CPU superscalaires ont évolués dans des directions différentes, chacun faisant à sa sauce, les choses sont devenues plus confuses. Le seul point commun est qu'ils ont eu tendance à rajouter des unités de calcul. Un point important est que la quadruple émission permet de nombreuses possibilités, mais la plupart ne sont pas intéressantes. Pour donner un exemple concret, prenons un CPU superscalaire très simple, comprenant une ALU entière, une FPU, une unité de branchement, et une unité mémoire. Il est possible de créer un CPU quadruple émission avec ces unités, mais le gain en performance n'en vaudrait pas la peine pour le cout en circuit. Il est rare qu'un bloc de 4 instructions contienne exactement une opération entière, une flottante, un branchement et un accès mémoire. En pratique, les seules combinaisons fréquentes sont les combinaisons calcul + accès mémoire et calcul + branchement. Les combinaisons calcul entier + calcul flottant sont aussi possibles, mais plus rares. Mais on dépasse rarement la double émission. Il est préférable d'utiliser ces unités dans un CPU double ou triple émission, en retirant l'unité de branchement et/ou en couplant l'unité mémoire à l'ALU. En clair, une fois arrivé à la quadruple émission, on ne peut plus se contenter d'une ALU, d'une FPU, d'une unité de branchement et d'une unité mémoire. Les processeurs superscalaires larges n'ont pas le choix que de rajouter des unités de calcul, voire des unités mémoire. Et en pratique, ils ont privilégié l'ajout d'ALU entières, avant de dupliquer les autres circuits. La raison est que les programmes informatiques utilisent beaucoup plus d'additions/soustractions/comparaisons que le reste. Il y a environ 5 opérations entières de base pour une multiplication entière, le ratio est le même quand on remplace la multiplication par un branchement ou un accès mémoire. En conséquence, les ALU entières ont été dupliquées en premier, et dans de nombreux exemplaires. Reste à voir comment a été dupliqué le reste. Les multiplieurs n'ont pas été dupliqués, sauf sur les processeurs superscalaires très larges. La raison est qu'il est rare de devoir exécuter deux multiplications à la fois. C'est la même chose pour le ''barrel shifter''. Deux branchements consécutifs sont quant à eux plus fréquents, c'est même assez courant. Aussi, les unités de branchement ont été dupliquées en second lieu, après les ALU entières. Avoir plusieurs accès mémoire proches est assez courant, surtout sur les CPU de type CISC. Aussi, les unités mémoire ont été dupliquées en troisième lieu. Pour résumer, dupliquer des ALU entières est donc la priorité, suivi par la duplication des unités de branchement, puis celle des accès mémoire, pour terminer par le multiplieur. La FPU est un peu à part. Une autre tendance a été de séparer l'unité mémoire des ALU entières, ce qui a permis l''''émission parallèle des µops mémoire'''. Comme son nom l'indique, l'idée est de séparer les µops mémoire des opérations entières. Il devient alors possible d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière. La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple. La '''double émission flottante''' a été utilisée sur les processeurs Alpha. L'idée est d'émettre deux opérations flottantes à la fois. Sauf qu'une implémentation naïve demanderait de dupliquer la FPU, ce qui aurait un cout en circuit rédhibitoire. Pour rappel, les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant. L'idée est d'avoir des ports d'émission séparés pour l'additionneur flottant et le multiplieur flottant. Le processeur peut alors émettre une addition flottante en même temps qu'une multiplication flottante. Les autres circuits de calcul flottant sont répartis sur ces deux ports d'émission flottants. L'avantage est que cela se marie bien avec l'usage d'une fenêtre d'instruction séparée pour les opérations flottantes. La fenêtre d'instruction a alors deux ports séparés, au lieu d'un seul. Rajouter un second port d'émission flottant n'est pas trop un problème, car le cout lié à l'ajout d'un port n'est pas linéaire. Passer de un port à deux a un cout tolérable, bien plus que de passer de 3 ports à 4 ou de 4 à 5. Il est possible de dupliquer l'additionneur et le multiplieur flottant, ce qui permet d'émettre deux additions et multiplications flottantes en même temps. C'est ce qui est fait sur les processeurs AMD d'architecture Zen 1 et 2. Ils ont deux additionneurs flottants par cœur, deux multiplieurs flottants, chacun avec leur propre port d'émission. Les performances en calcul flottant sont assez impressionnantes pour un processeur de l'époque. [[File:ZEN - émission multiple flottante.png|centre|vignette|upright=2|Microarchitecture Zen 1 d'AMD.]] ==Les ports d'émission et de décodage== Les ports d'émission et de décodage sont des ressources matérielles comme les autres. Dans cette section, nous allons parler de concepts liés aux ports d'émission et de décodage. Nous allons notamment parler du partage des ports d'émission, ainsi que des relations numériques entre ces deux types de ports. ===Le partage des ports d'émission et la ''port contention''=== Il est possible de partager un port d'émission, à savoir de connecter un port d'émission à plusieurs unités de calcul. Nous en avons vu un exemple plus haut, avec l'émission multiple des opérations entières. On peut par exemple connecter un multiplieur et une ALU entière sur le même port d'émission. Pour le moment, nous n'avons vu que des cas où un port d'émission est relié soit à des ALU entières, soit à des FPU, pas aux deux. Mais il existe des processeurs où un même port d'émission alimente à la fois une ALU entière et une FPU. Par exemple, on peut relier un additionneur flottant sur le même port qu'une ALU entière. Il faut noter que cela implique une fenêtre d'instruction centralisée, capable de mettre en attente micro-opérations entières et flottantes. Un exemple est celui des processeurs Core 2 Duo. Ils disposent de 6 ports d'émission, dont 3 ports dédiés à l'unité mémoire. Les 3 ports restants alimentent chacun : une ALU entière, un circuit de calcul flottant et une unité de calcul SSE (une unité de calcul SIMD, qu'on abordera dans quelques chapitres). * Le premier port alimente une ALU entière simple et un multiplieur/diviseur flottant. * Le second alimente une ALU entière, un multiplieur entier et un additionneur flottant. * Le troisième alimente une ALU entière, sans circuit flottant dédié. Dans le même genre, il est possible de partager un port d'émission entre l'unité mémoire et une ALU entière. Cela permet d'utiliser l'ALU entière pour les calculs d'adresse, ce qui évite d'avoir à utiliser une unité de calcul d'adresse distincte. Un exemple est celui du processeur superscalaire double émission Power PC 440. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. L'organisation en question permet soit d'émettre un accès mémoire en même temps qu'une opération entière, soit d'émettre deux opérations entières simples, soit d’émettre une multiplication et une addition/soustraction/comparaison. Une organisation simple, mais très efficace ! [[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]] Regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles, qui ne se présentent que pour certaines combinaisons de micro-opérations bien précises. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port. Le nombre de ports peut être un facteur limitant pour la performance dans certaines situations de '''''port contention''''' où on a assez d'ALU pour exécuter N micro-opérations, mais où la répartition des ALUs sur les ports l’empêche. Mais on peut réduire la ''port contention'' en répartissant correctement les ALU sur les ports d'émission. Le choix en question dépendent fortement du fait qu'en moyenne, que certaines paires d'instructions sont plus fréquentes que d'autres. Tout cela guide ce choix de répartition des ALUs sur les ports. Nous venons de voir que la répartition des ALU sur les ports d'émission est très variable d'un processeur à l'autre. Les divers choix possibles sont tous des compromis entre deux forces : réduire le nombre de ports d'émission d'un côté, garder de bonnes performances en réduisant la ''port contention'' de l'autre. Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction relativement simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ? Par contre, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal réparti les ALU. ===La séparation entre largeur de décodage et d'émission=== Sur les processeurs superscalaires larges, le processeur peut émettre plus d'instructions qu'il peut en décoder. Plus haut, j'ai dit que l'unité d'émission avait parfois plus de ports d'émission que de ports de décodage. La raison est assez simple, et elle s'explique assez bien avec un exemple. Prenons un processeur avec trois ALU entières, une FPU, une unité de branchement et une unité mémoire. Chacune a son propre port d'émission, ce qui fait que le CPU est capable d'émettre 6 µops. Du moins, c'est la théorie, car les conditions pour sont assez rares. Il faut qu'en chargeant un paquet de 6 instructions, il y ait exactement trois instructions entières, une flottante, un branchement et un accès mémoire. Si le paquet contient 5 instructions entières et un branchement, seules quatre instructions sont émises. Dans cet exemple, dans la grande majorité des cas, seuls 4 ports seront utilisés en moyenne et deux seront sous-utilisés. Il s'agit là d'une moyenne, qui dépend du code, et des instructions exactes à exécuter. Cependant, cette moyenne nous donne un moyen d'économiser beaucoup de circuits pour un cout en performance mineur. L'idée est de ne charger que des blocs de 4 instructions, mais d'avoir 6 ports d'émission. Le processeur charge et décode 4 instructions, les quatre instructions sont envoyées sur les ports d'émission adéquat. Le cout en transistors est alors situé entre celui d'un CPU quadruple émission et sextuple émission. Et les performances sont aussi entre les deux. Pour les performances, c'est parce que cela permet d'avoir un CPU quadruple émission avec bien moins de contraintes d’appariement. Imaginez qu'on se soit limité à quatre ports d'émission : il aurait fallu mettre plusieurs unités sur le même port d'émission. En conséquence, il y aurait eu des combinaisons interdites, comme émettre deux instructions entières + un branchement + un accès mémoire ; ou deux instructions entières + une opération flottante + un accès mémoire. De tells appariement interdits sont possibles en utilisant les six ports d'émission. ==L'émission multiple des accès mémoire== Les premiers processeurs superscalaires ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles. Par contre, c'est autre chose sur les processeurs superscalaires larges. Vu qu'ils travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares. L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. La ''load/store queue'' reste une structure unique, mais on peut lui rajouter des ports. Et à ce peit jeu, il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire. La première duplique les unités de calcul, mais pas les ports d'accès au cache. Les accès mémoire sont alors accumulés dans la ''load/store queue'', qui sont ensuite traités un par un par le cache. Un processeur utilisant cette technique est, par exemple, capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. Un exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. Ces processeurs pouvaient calculer trois adresses simultanément, mais ne pouvaient faire qu'un seul accès au cache par cycle. Le cout en circuit est modéré : les unités de calcul coutent très peu, les ports de la ''load/store queue'' coutent déjà un peu plus. La seconde méthode ajoute des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultanées. Il faut alors dupliquer des unités de calcul, rajouter les ports sur le cache, mais aussi rajouter des ports sur la ''load/store queue'', pour qu'elle échange des données/commandes mémoire avec le cache. Le cout en circuits est alors conséquent Quelques processeurs utilisaient des solutions intermédiaires, un exemple classique étant les Pentium 1 et 2 d'Intel. Mais nous en parlerons dans le chapitre suivant. [[File:Processeur non-superscalaire basique 02.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]] Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre. Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Le Pentium 4 était dans ce cas, mais il était un peu seul dans sa catégorie. Un cas similaire est celui où l'unité mémoire est alimentée par une fenêtre de micro-opération mélangeant instructions mémoire et entières. Les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. Dans ce cas, l'émission multiple est rarement implémentée. Le cas suivant a une file pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique. Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui pouvaient seulement émettre une lecture en même temps qu'une écriture. Ils avaient deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse. [[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]] Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture demande d'utiliser une file d'écriture multiport, ce qui est assez compliqué. Implémenter des lectures en plus est plus simple : il suffit de rajouter une seconde file de µops LOAD et une unité de calcul, alimentées via un second port d'émission. Il est aussi possible de rajouter un second port de lecture au cache, mais ce n'est pas obligatoire. Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse. De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports de lecture et d'écriture car il y a plus de lecture que d'écritures. Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. : Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse. [[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]] Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettent plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail. Pour résumer, l'évolution des CPU superscalaires a été la suivante : * pipeline mémorie fusionné avec le pipeline entier ; * séparation du pipeline mémoire du reste, avec émission parallèle des accès mémoire ; * émission multiple, mais sans dupliquer les ports du cache ; * émission multiple, avec émission simultanée d'une lecture et d’une écriture dans le cache ; * émission multiple avec plusieurs accès en lecture au cache. ==Les optimisations des processeurs superscalaires larges== Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul. ===L'étape de chargement d'un CPU superscalaire large=== Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 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 autoaligné.]] Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements ! [[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]] Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez 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 microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total). Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois 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 microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement. Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations. ===La micro-fusion des processeurs Intel et AMD=== La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''. Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''. Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout. La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Le parallélisme mémoire | prevText=Le parallélisme mémoire | next=Exemples de microarchitectures CPU : le cas du x86 | nextText=Exemples de microarchitectures CPU : le cas du x86 }} </noinclude> 0uz885k23118xy0yn7ietfa7s8bfxvd 772673 772672 2026-09-21T14:23:35Z Mewtow 31375 /* L’impact de la superscalarité sur les autres circuits */ 772673 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 tout premiers processeurs superscalaires pouvaient émettre deux µops en même temps, ce qui portait le nom de '''double émission'''. Mais le nombre a rapidement augmenté et les processeurs modernes 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. ==L'implémentation des processeurs superscalaires== Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre. Intuitivement, émettre et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Il est aussi possible de dupliquer les unités de calcul pour exécuter plusieurs µops en même temps. Mais d'autres circuits sont simplement modifiés. Par exemple, l'unité de chargement n'est pas dupliquée, mais simplement modifiée, idem pour l'unité d'émission et les circuits d'exécution dans le désordre. Voyons ce qu'il en est dans le détail. ===Les unités de calcul d'un processeur superscalaire=== Émettre N instructions simultanément demande implicitement d'exécuter N instructions par cycle, voire plus si des instructions multicycles viennent mettre leur grain de sel. Intuitivement, on se dit que les unités de calcul doivent être dupliquées. Et c'est une solution viable pour les processeurs très simples, avec seulement quelques ALU entières. Par exemple, un processeur RISC basique avec seulement une ALU entière et un ''barrel shifter'' peut être rendu superscalaire en dupliquant les deux circuits. Le problème, est que cette solution marche assez mal dès que le processeur contient des circuits multiplieurs et/ou une FPU. Par exemple, un processeur à double émission devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, etc. 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 ! Par contre, l'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission. Un autre cas extrême est celui où aucune unité de calcul n'est dupliquée. Pour comprendre comment c'est possible rappelez-vous qu'un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et éventuellement une unité de branchement. Cela suffit pour créer un processeur quadruple émission, très limité. Un tel processeur peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire. Le problème est qu'il est rare que l'on ait à exécuter en même temps un calcul entier, un calcul flottant, un accès mémoire et un branchement. La quadruple émission n'est donc que rarement utilisée. En pratique, les seules combinaisons fréquentes sont les combinaisons calcul + accès mémoire et calcul + branchement. Les combinaisons calcul entier + calcul flottant sont aussi possibles, mais plus rares. Mais on dépasse rarement la double émission. En clair, à partir de la quadruple émission, on ne peut plus se contenter d'une ALU, d'une FPU, d'une unité de branchement et d'une unité mémoire. Les processeurs superscalaires larges n'ont pas le choix que de rajouter des unités de calcul. Reste à voir ce qui est dupliqué en pratique. 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. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant. La conséquence est que les processeurs superscalaires ont des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a des paires d'instructions autorisées et des paires interdites. Par exemple, l'exécution simultanée de deux branchements est interdite, les branchements sont exécutés l'un après l'autre. Mais il est possible d'émettre un branchement en même temps qu'une autre instruction, en espérant que la prédiction de branchement ait fait une bonne prédiction. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur. En pratique, ces contraintes d’appariement ont peu d’impact. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière, le ratio est le même quand on remplace la multiplication par un branchement ou un accès mémoire. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Deux branchements consécutifs sont quant à eux plus fréquents, idem pour deux accès mémoire. Dupliquer des ALU entières est donc la priorité, suivi par la duplication des unités de branchement, puis celle des accès mémoire, pour terminer par le multiplieur et la FPU. ===Le chargement de plusieurs instructions consécutives=== Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large. Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence. Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas. Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement non-pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc. [[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]] ===Le décodage parallèle des instructions=== Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits. Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur. Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! ===L'unité d'émission d'un processeur superscalaire=== La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. Nous verrons pourquoi dans la suite, mais on peut dire que c'est juste plus simple et plus efficace ainsi. Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop candidate, sous entendu "candidate à l'émission". L'unité d'émission détecte les dépendances entre la µop candidate et les µops en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en cours d'exécution dans le pipeline. mais en plus, elle doit tester les dépendances entre les µops candidates. Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément, il doit détecter les paires d'instructions dépendantes et les sérialiser. Avec une fenêtre d'instruction, il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil gérent les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires modernes utilisent tous une fenêtre d'instruction centralisée ou décentralisée, et non des ''scoreboard''. Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps, là encore en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire peut émettre 8 µops à la fois, on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle. ===L'unité de renommage superscalaire=== Avec la superscalarité, l'unité de renommage de registre doit gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance de registre, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées. Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat. [[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]] Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante. [[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]] Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande. [[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]] ===Le banc de registre d'un processeur superscalaire=== Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement. 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. ===Résumé=== Pour résumer le tout, voici ce que cela donne dans les grandes lignes. * L'unité de chargement charge deux fois plus de données, mais n'est pas modifiée en profondeur. * Les décodeurs sont dupliqués, ce qui a un cout en circuit important. * Les circuits d'émission et d’exécution dans le désordre sont légèrement altérés. * Les unités de calcul sont souvent dupliquées, sauf sur les tout premiers CPU superscalaires. * Le banc de registre a plus de ports. {|class="wikitable" |- ! rowspan="2" | Processeur sans émission multiple | rowspan="2" | Chargement | rowspan="2" | Décodage | rowspan="2" | Renommage | rowspan="2" | Émission | Exécution / ALU | rowspan="2" | ''Commit''/ROB |- | Exécution / ALU |- ! rowspan="4" | Processeur superscalaire | rowspan="4" | Chargement | rowspan="2" | Décodage | rowspan="4" | Renommage | rowspan="4" | Émission | Exécution / ALU | rowspan="4" | ''Commit''/ROB |- | Exécution / ALU |- | rowspan="2" | Décodage | Exécution / ALU |- | Exécution / ALU |} Pour les unités de calcul, tout dépend du processeur. Il existe même des processeurs sur lesquels aucune unité de calcul n'est ajoutée ! Et pour comprendre pourquoi, nous allons parler des processeurs superscalaires historiques. ==L'évolution historique des processeurs superscalaires== Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs triple émission permettent eux d’émettre trois instructions à la fois. Les tout premiers processeurs superscalaires avaient un budget en transistors très limité, ce qui fait qu'ils ne pouvaient pas être "totalement superscalaires". Par exemple, un processeur double émission ne pouvait pas émettre deux opérations entières en même temps, ni deux opérations flottantes, ni une opération entière avec un accès mémoire. Il y avait des paires d'instructions autorisées et des paires interdites. Les '''contraintes d'appariement''' sur les instructions à émettre en même temps étaient très strictes. L'intérêt de ces contraintes d’appariement est que le processeur était très simple, il n'y avait pas besoin de rajouter des circuits complexes pour rendre le processeur superscalaire. Les CPU superscalaires historiques ne faisaient que mieux utiliser des unités de calcul déjà présentes. Prenons un processeur non-superscalaire simple, illustré ci-dessous. Il contient : une unité de chargement, un décodeur, une unité d'émission, une ALU, une FPU, les bancs de registre entier et flottants, et une unité mémoire. Nous avons placé l'unité mémoire à la suite de l'ALU entière, car ce sera important pour les explications qui vont suivre. [[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]] Les processeurs superscalaires historiques modifiaient assez peu le schéma précédent. L'unité de chargement était adaptée pour lire plusieurs instructions depuis la mémoire, il y avait un seul décodeur, l'unité d'émission était assez peu modifiée. Mais une chose était certaine : aucune unité de calcul n'était dupliquée, le banc de registre n'était presque pas modifié. A la rigueur, gérer la triple 'émission avec les branchements demandait parfois d'ajouter des ports de lecture sur le banc de registre généraux, pas plus. ===La double émission entière-flottante=== Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est d'émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Le processeur a donc deux pipelines séparés : un pour les micro-opérations entières, un autre pour les micro-opérations flottantes. On parle alors de '''double émission entière-flottante'''. La mise en œuvre utilise assez peu de circuits, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité. La plupart des premiers processeurs superscalaires étaient de ce type. Les exemples les plus notables sont les processeurs POWER 1 et ses dérivés comme le ''RISC Single Chip''. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle. [[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]] La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisations visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate]. ===L'émission parallèle des branchements=== Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur. L'idée était d'appliquer un équivalent de la double émission, non pas pour des opérations flottantes, mais pour autre chose. Il y avait deux candidats : l'unité de branchement, l'unité mémoire. Et c'est l'unité de branchement qui a été choisie. Le processeur pouvait émettre un branchement en parallèle d'une instruction entière. La technique s'appelle l''''émission parallèle des branchements'''. Combinée à la double émission entière-flottante, cela permettait d'émettre trois µops en même temps : un branchement, une opération flottante, et une opération entière/mémoire. Les exemples de processeurs de ce type ne sont pas très nombreux, mais on peut citer l'Intel i960CA et l'HyperSPARC. Les branchements étant fréquents, surtout sur les CPU de type RISC, les gains en performances étaient assez impressionnants. Quant au cout en transistor, il dépendait du jeu d’instruction, expliquons pourquoi. Pour les processeurs avec un registre d'état, l'implémentation est particulièrement simple. L'unité de branchement lit ce registre d'état, sélectionne le bit adéquat, teste sa valeur, et modifie le ''program counter'' en fonction. Le processeur intègre déjà une unité de branchement séparée de l'unité de calcul, pour faire tout cela. Pas besoin d'utiliser l'unité de calcul entière, ni toute autre unité. Il n'y a même pas besoin de toucher au banc de registres, vu que le registre d'état n'est pas dedans, idem pour le ''program counter'' Et il en est de même si le processeur utiliser des registres à prédicats. Là encore, il y a un banc de registre séparé pour les prédicats et une unité de branchement dédiée. L'implémentation de la triple émission ne fait alors qu'utiliser au mieux des circuits existants. Sur les CPU de type RISC, sans registre d'état, les choses sont cependant différentes. De tels processeurs ont des instructions de branchements qui lisent deux opérandes dans les registres, les comparent, et altèrent ou non le ''program counter'' suivant le résultat. L'implémentation de la triple émission demande alors d'ajouter deux ports de lecture au banc de registre entier, pour lire les opérandes. Il faut aussi ajouter une unité pour faire la comparaison, si elle n'était pas déjà présente. Certains CPU RISC utilisent une unité de branchement séparée, mais d'autres utilisaient l'ALU entière pour. ===L'émission multiple des micro-opérations entières=== Voyons maintenant l'étape après la triple émission entière-flottante-branchement. Elle est assez simple, à savoir qu'elle permet d'émettre deux instructions entières en même temps, avec éventuellement une instruction flottante en plus. Il est parfois possible de monter à trois ou quatre instructions entières. 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 modifier le banc de registres. Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. [[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]] Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications et les divisions ? Pour étudier ces questions, nous allons prendre le cas d'un processeur capable d’émettre trois opérations en même temps : deux opérations entières, et une flottante, potentiellement d'autres. Le cas des opérations complexes est assez intéressant. Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les ALUs en N exemplaires. Mais ce n'est jamais fait en pratique. Concrètement, seules les ALU entières simples le sont, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ne sont pas dupliqués en N exemplaires, le cout en transistors serait trop important. Pour un processeur à double émission, l'unité d'émission a deux ports d'émission, pour émettre deux µops entières. Les deux ports ont chacun leur ALU entière, mais l'un d'entre eux a est aussi relié au circuit multiplier. En clair, cela permet d'émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Idem pour les opérations simples comme les soustractions, les opérations bit à bit, etc. On peut en exécuter deux, ou en coupler une avec une multiplication. Faire ainsi n'a pas de cout en performance significatif, comparé à une duplication totale des ALUs entières. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur serait inutile. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle. [[File:Emission multiple des opérations entières, implémentation naive.png|centre|vignette|upright=2|Émission multiple des opérations entières, implémentation naïve.]] Il en est de même avec les décalages, vu que le ''barrel shifter'' n'est pas dupliqué. Il est donc possible d'émettre deux opérations de base, un décalage avec une opération de base, mais pas deux décalages en parallèle. En général, le ''barrel shifter'' et le multiplieur sont placés sur deux ports d'émission séparés. Cela permet ainsi de faire un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas. [[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Emission multiple des opérations entières, implémentation courante]] Pour les branchements, deux solutions sont possibles. Avec la première, il y a émission parallèle des branchements, à savoir qu'il est possible d'émettre en branchement en parallèle de deux-trois instructions entières. Et cela impose la présence d'une unité de branchement séparée, avec son port d'émission. Avec la seconde, les branchements sont traités comme des instructions entières. Il peut y avoir une unité de branchement séparée, mais ce n'est pas souvent le cas. En général, les branchements sont exécutés dans une ALU entière. [[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Emission série et parallèle des branchements]] Les accès mémoire sont traités comme des opérations entières un peu spéciales, sur les processeurs superscalaires historiques. En clair, le processeur peut émettre deux opérations entières, ou une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent. Les raisons à cela étaient nombreuses. Et ces raisons sont encore valables sur des petits processeurs destinés à l'embarqué ou l'informatique industrielle. Seuls les processeurs haute performance modernes font différemment. La raison principale est le cout en circuit. Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse. Pas besoin de rajouter une unité de calcul d'adresse séparée. Mais surtout, permettre l'émission parallèle des accès mémoire ferait passer un processeur triple émission à de la quadruple émission, avec tous les couts que ça implique. Il faudrait rajouter des ports de lecture/écriture au banc de registre généraux, pour traiter un accès mémoire en plus du reste. Les circuits d'émission et de chargement devraient être élargit, aussi. Et quitte à payer un tel cout en circuit, il serait plus rentable de l'utiliser pour émettre une quatrième opération entière qu'un accès mémoire. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand-chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2. ===Les CPU superscalaires à plusieurs unités spécialisées=== Le classique deux opérations entières + une flottante était l'idéal pour la triple émission. Même les processeurs modernes à triple émission restent sur cette "norme", car elle est proche de l'optimal. Mais arrivé à la quadruple émission, les possibilités sont devenues plus diverses. L'évolution des CPU superscalaires ont évolués dans des directions différentes, chacun faisant à sa sauce, les choses sont devenues plus confuses. Le seul point commun est qu'ils ont eu tendance à rajouter des unités de calcul. Un point important est que la quadruple émission permet de nombreuses possibilités, mais la plupart ne sont pas intéressantes. Pour donner un exemple concret, prenons un CPU superscalaire très simple, comprenant une ALU entière, une FPU, une unité de branchement, et une unité mémoire. Il est possible de créer un CPU quadruple émission avec ces unités, mais le gain en performance n'en vaudrait pas la peine pour le cout en circuit. Il est rare qu'un bloc de 4 instructions contienne exactement une opération entière, une flottante, un branchement et un accès mémoire. En pratique, les seules combinaisons fréquentes sont les combinaisons calcul + accès mémoire et calcul + branchement. Les combinaisons calcul entier + calcul flottant sont aussi possibles, mais plus rares. Mais on dépasse rarement la double émission. Il est préférable d'utiliser ces unités dans un CPU double ou triple émission, en retirant l'unité de branchement et/ou en couplant l'unité mémoire à l'ALU. En clair, une fois arrivé à la quadruple émission, on ne peut plus se contenter d'une ALU, d'une FPU, d'une unité de branchement et d'une unité mémoire. Les processeurs superscalaires larges n'ont pas le choix que de rajouter des unités de calcul, voire des unités mémoire. Et en pratique, ils ont privilégié l'ajout d'ALU entières, avant de dupliquer les autres circuits. La raison est que les programmes informatiques utilisent beaucoup plus d'additions/soustractions/comparaisons que le reste. Il y a environ 5 opérations entières de base pour une multiplication entière, le ratio est le même quand on remplace la multiplication par un branchement ou un accès mémoire. En conséquence, les ALU entières ont été dupliquées en premier, et dans de nombreux exemplaires. Reste à voir comment a été dupliqué le reste. Les multiplieurs n'ont pas été dupliqués, sauf sur les processeurs superscalaires très larges. La raison est qu'il est rare de devoir exécuter deux multiplications à la fois. C'est la même chose pour le ''barrel shifter''. Deux branchements consécutifs sont quant à eux plus fréquents, c'est même assez courant. Aussi, les unités de branchement ont été dupliquées en second lieu, après les ALU entières. Avoir plusieurs accès mémoire proches est assez courant, surtout sur les CPU de type CISC. Aussi, les unités mémoire ont été dupliquées en troisième lieu. Pour résumer, dupliquer des ALU entières est donc la priorité, suivi par la duplication des unités de branchement, puis celle des accès mémoire, pour terminer par le multiplieur. La FPU est un peu à part. Une autre tendance a été de séparer l'unité mémoire des ALU entières, ce qui a permis l''''émission parallèle des µops mémoire'''. Comme son nom l'indique, l'idée est de séparer les µops mémoire des opérations entières. Il devient alors possible d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière. La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple. La '''double émission flottante''' a été utilisée sur les processeurs Alpha. L'idée est d'émettre deux opérations flottantes à la fois. Sauf qu'une implémentation naïve demanderait de dupliquer la FPU, ce qui aurait un cout en circuit rédhibitoire. Pour rappel, les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant. L'idée est d'avoir des ports d'émission séparés pour l'additionneur flottant et le multiplieur flottant. Le processeur peut alors émettre une addition flottante en même temps qu'une multiplication flottante. Les autres circuits de calcul flottant sont répartis sur ces deux ports d'émission flottants. L'avantage est que cela se marie bien avec l'usage d'une fenêtre d'instruction séparée pour les opérations flottantes. La fenêtre d'instruction a alors deux ports séparés, au lieu d'un seul. Rajouter un second port d'émission flottant n'est pas trop un problème, car le cout lié à l'ajout d'un port n'est pas linéaire. Passer de un port à deux a un cout tolérable, bien plus que de passer de 3 ports à 4 ou de 4 à 5. Il est possible de dupliquer l'additionneur et le multiplieur flottant, ce qui permet d'émettre deux additions et multiplications flottantes en même temps. C'est ce qui est fait sur les processeurs AMD d'architecture Zen 1 et 2. Ils ont deux additionneurs flottants par cœur, deux multiplieurs flottants, chacun avec leur propre port d'émission. Les performances en calcul flottant sont assez impressionnantes pour un processeur de l'époque. [[File:ZEN - émission multiple flottante.png|centre|vignette|upright=2|Microarchitecture Zen 1 d'AMD.]] ==Les ports d'émission et de décodage== Les ports d'émission et de décodage sont des ressources matérielles comme les autres. Dans cette section, nous allons parler de concepts liés aux ports d'émission et de décodage. Nous allons notamment parler du partage des ports d'émission, ainsi que des relations numériques entre ces deux types de ports. ===Le partage des ports d'émission et la ''port contention''=== Il est possible de partager un port d'émission, à savoir de connecter un port d'émission à plusieurs unités de calcul. Nous en avons vu un exemple plus haut, avec l'émission multiple des opérations entières. On peut par exemple connecter un multiplieur et une ALU entière sur le même port d'émission. Pour le moment, nous n'avons vu que des cas où un port d'émission est relié soit à des ALU entières, soit à des FPU, pas aux deux. Mais il existe des processeurs où un même port d'émission alimente à la fois une ALU entière et une FPU. Par exemple, on peut relier un additionneur flottant sur le même port qu'une ALU entière. Il faut noter que cela implique une fenêtre d'instruction centralisée, capable de mettre en attente micro-opérations entières et flottantes. Un exemple est celui des processeurs Core 2 Duo. Ils disposent de 6 ports d'émission, dont 3 ports dédiés à l'unité mémoire. Les 3 ports restants alimentent chacun : une ALU entière, un circuit de calcul flottant et une unité de calcul SSE (une unité de calcul SIMD, qu'on abordera dans quelques chapitres). * Le premier port alimente une ALU entière simple et un multiplieur/diviseur flottant. * Le second alimente une ALU entière, un multiplieur entier et un additionneur flottant. * Le troisième alimente une ALU entière, sans circuit flottant dédié. Dans le même genre, il est possible de partager un port d'émission entre l'unité mémoire et une ALU entière. Cela permet d'utiliser l'ALU entière pour les calculs d'adresse, ce qui évite d'avoir à utiliser une unité de calcul d'adresse distincte. Un exemple est celui du processeur superscalaire double émission Power PC 440. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. L'organisation en question permet soit d'émettre un accès mémoire en même temps qu'une opération entière, soit d'émettre deux opérations entières simples, soit d’émettre une multiplication et une addition/soustraction/comparaison. Une organisation simple, mais très efficace ! [[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]] Regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles, qui ne se présentent que pour certaines combinaisons de micro-opérations bien précises. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port. Le nombre de ports peut être un facteur limitant pour la performance dans certaines situations de '''''port contention''''' où on a assez d'ALU pour exécuter N micro-opérations, mais où la répartition des ALUs sur les ports l’empêche. Mais on peut réduire la ''port contention'' en répartissant correctement les ALU sur les ports d'émission. Le choix en question dépendent fortement du fait qu'en moyenne, que certaines paires d'instructions sont plus fréquentes que d'autres. Tout cela guide ce choix de répartition des ALUs sur les ports. Nous venons de voir que la répartition des ALU sur les ports d'émission est très variable d'un processeur à l'autre. Les divers choix possibles sont tous des compromis entre deux forces : réduire le nombre de ports d'émission d'un côté, garder de bonnes performances en réduisant la ''port contention'' de l'autre. Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction relativement simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ? Par contre, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal réparti les ALU. ===La séparation entre largeur de décodage et d'émission=== Sur les processeurs superscalaires larges, le processeur peut émettre plus d'instructions qu'il peut en décoder. Plus haut, j'ai dit que l'unité d'émission avait parfois plus de ports d'émission que de ports de décodage. La raison est assez simple, et elle s'explique assez bien avec un exemple. Prenons un processeur avec trois ALU entières, une FPU, une unité de branchement et une unité mémoire. Chacune a son propre port d'émission, ce qui fait que le CPU est capable d'émettre 6 µops. Du moins, c'est la théorie, car les conditions pour sont assez rares. Il faut qu'en chargeant un paquet de 6 instructions, il y ait exactement trois instructions entières, une flottante, un branchement et un accès mémoire. Si le paquet contient 5 instructions entières et un branchement, seules quatre instructions sont émises. Dans cet exemple, dans la grande majorité des cas, seuls 4 ports seront utilisés en moyenne et deux seront sous-utilisés. Il s'agit là d'une moyenne, qui dépend du code, et des instructions exactes à exécuter. Cependant, cette moyenne nous donne un moyen d'économiser beaucoup de circuits pour un cout en performance mineur. L'idée est de ne charger que des blocs de 4 instructions, mais d'avoir 6 ports d'émission. Le processeur charge et décode 4 instructions, les quatre instructions sont envoyées sur les ports d'émission adéquat. Le cout en transistors est alors situé entre celui d'un CPU quadruple émission et sextuple émission. Et les performances sont aussi entre les deux. Pour les performances, c'est parce que cela permet d'avoir un CPU quadruple émission avec bien moins de contraintes d’appariement. Imaginez qu'on se soit limité à quatre ports d'émission : il aurait fallu mettre plusieurs unités sur le même port d'émission. En conséquence, il y aurait eu des combinaisons interdites, comme émettre deux instructions entières + un branchement + un accès mémoire ; ou deux instructions entières + une opération flottante + un accès mémoire. De tells appariement interdits sont possibles en utilisant les six ports d'émission. ==L'émission multiple des accès mémoire== Les premiers processeurs superscalaires ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles. Par contre, c'est autre chose sur les processeurs superscalaires larges. Vu qu'ils travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares. L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. La ''load/store queue'' reste une structure unique, mais on peut lui rajouter des ports. Et à ce peit jeu, il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire. La première duplique les unités de calcul, mais pas les ports d'accès au cache. Les accès mémoire sont alors accumulés dans la ''load/store queue'', qui sont ensuite traités un par un par le cache. Un processeur utilisant cette technique est, par exemple, capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. Un exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. Ces processeurs pouvaient calculer trois adresses simultanément, mais ne pouvaient faire qu'un seul accès au cache par cycle. Le cout en circuit est modéré : les unités de calcul coutent très peu, les ports de la ''load/store queue'' coutent déjà un peu plus. La seconde méthode ajoute des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultanées. Il faut alors dupliquer des unités de calcul, rajouter les ports sur le cache, mais aussi rajouter des ports sur la ''load/store queue'', pour qu'elle échange des données/commandes mémoire avec le cache. Le cout en circuits est alors conséquent Quelques processeurs utilisaient des solutions intermédiaires, un exemple classique étant les Pentium 1 et 2 d'Intel. Mais nous en parlerons dans le chapitre suivant. [[File:Processeur non-superscalaire basique 02.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]] Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre. Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Le Pentium 4 était dans ce cas, mais il était un peu seul dans sa catégorie. Un cas similaire est celui où l'unité mémoire est alimentée par une fenêtre de micro-opération mélangeant instructions mémoire et entières. Les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. Dans ce cas, l'émission multiple est rarement implémentée. Le cas suivant a une file pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique. Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui pouvaient seulement émettre une lecture en même temps qu'une écriture. Ils avaient deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse. [[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]] Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture demande d'utiliser une file d'écriture multiport, ce qui est assez compliqué. Implémenter des lectures en plus est plus simple : il suffit de rajouter une seconde file de µops LOAD et une unité de calcul, alimentées via un second port d'émission. Il est aussi possible de rajouter un second port de lecture au cache, mais ce n'est pas obligatoire. Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse. De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports de lecture et d'écriture car il y a plus de lecture que d'écritures. Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. : Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse. [[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]] Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettent plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail. Pour résumer, l'évolution des CPU superscalaires a été la suivante : * pipeline mémorie fusionné avec le pipeline entier ; * séparation du pipeline mémoire du reste, avec émission parallèle des accès mémoire ; * émission multiple, mais sans dupliquer les ports du cache ; * émission multiple, avec émission simultanée d'une lecture et d’une écriture dans le cache ; * émission multiple avec plusieurs accès en lecture au cache. ==Les optimisations des processeurs superscalaires larges== Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul. ===L'étape de chargement d'un CPU superscalaire large=== Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 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 autoaligné.]] Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements ! [[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]] Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez 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 microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total). Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois 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 microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement. Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations. ===La micro-fusion des processeurs Intel et AMD=== La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''. Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''. Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout. La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Le parallélisme mémoire | prevText=Le parallélisme mémoire | next=Exemples de microarchitectures CPU : le cas du x86 | nextText=Exemples de microarchitectures CPU : le cas du x86 }} </noinclude> 2cr29ny50rq15s20gt4bkno5zykez7g 772674 772673 2026-09-21T14:26:00Z Mewtow 31375 /* L'implémentation des processeurs superscalaires */ 772674 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 tout premiers processeurs superscalaires pouvaient émettre deux µops en même temps, ce qui portait le nom de '''double émission'''. Mais le nombre a rapidement augmenté et les processeurs modernes 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. ==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. Par exemple, l'unité de chargement charge deux fois plus de données, mais n'est pas modifiée en profondeur. L'unité d'émission et les circuits d'exécution dans le désordre sont eux aussi altérés. Les unités de calcul sont souvent dupliquées, sauf sur les tout premiers CPU superscalaires. {|class="wikitable" |- ! rowspan="2" | Processeur sans émission multiple | rowspan="2" | Chargement | rowspan="2" | Décodage | rowspan="2" | Renommage | rowspan="2" | Émission | Exécution / ALU | rowspan="2" | ''Commit''/ROB |- | Exécution / ALU |- ! rowspan="4" | Processeur superscalaire | rowspan="4" | Chargement | rowspan="2" | Décodage | rowspan="4" | Renommage | rowspan="4" | Émission | Exécution / ALU | rowspan="4" | ''Commit''/ROB |- | Exécution / ALU |- | rowspan="2" | Décodage | Exécution / ALU |- | Exécution / ALU |} Voyons ce qu'il en est dans le détail. ===Les unités de calcul d'un processeur superscalaire=== Émettre N instructions simultanément demande implicitement d'exécuter N instructions par cycle, voire plus si des instructions multicycles viennent mettre leur grain de sel. Intuitivement, on se dit que les unités de calcul doivent être dupliquées. Et c'est une solution viable pour les processeurs très simples, avec seulement quelques ALU entières. Par exemple, un processeur RISC basique avec seulement une ALU entière et un ''barrel shifter'' peut être rendu superscalaire en dupliquant les deux circuits. Le problème, est que cette solution marche assez mal dès que le processeur contient des circuits multiplieurs et/ou une FPU. Par exemple, un processeur à double émission devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, etc. 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 ! Par contre, l'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission. Un autre cas extrême est celui où aucune unité de calcul n'est dupliquée. Pour comprendre comment c'est possible rappelez-vous qu'un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et éventuellement une unité de branchement. Cela suffit pour créer un processeur quadruple émission, très limité. Un tel processeur peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire. Le problème est qu'il est rare que l'on ait à exécuter en même temps un calcul entier, un calcul flottant, un accès mémoire et un branchement. La quadruple émission n'est donc que rarement utilisée. En pratique, les seules combinaisons fréquentes sont les combinaisons calcul + accès mémoire et calcul + branchement. Les combinaisons calcul entier + calcul flottant sont aussi possibles, mais plus rares. Mais on dépasse rarement la double émission. En clair, à partir de la quadruple émission, on ne peut plus se contenter d'une ALU, d'une FPU, d'une unité de branchement et d'une unité mémoire. Les processeurs superscalaires larges n'ont pas le choix que de rajouter des unités de calcul. Reste à voir ce qui est dupliqué en pratique. 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. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant. La conséquence est que les processeurs superscalaires ont des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a des paires d'instructions autorisées et des paires interdites. Par exemple, l'exécution simultanée de deux branchements est interdite, les branchements sont exécutés l'un après l'autre. Mais il est possible d'émettre un branchement en même temps qu'une autre instruction, en espérant que la prédiction de branchement ait fait une bonne prédiction. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur. En pratique, ces contraintes d’appariement ont peu d’impact. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière, le ratio est le même quand on remplace la multiplication par un branchement ou un accès mémoire. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Deux branchements consécutifs sont quant à eux plus fréquents, idem pour deux accès mémoire. Dupliquer des ALU entières est donc la priorité, suivi par la duplication des unités de branchement, puis celle des accès mémoire, pour terminer par le multiplieur et la FPU. ===Le chargement de plusieurs instructions consécutives=== Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large. Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence. Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas. Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement non-pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc. [[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]] ===Le décodage parallèle des instructions=== Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits. Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur. Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! ===L'unité d'émission d'un processeur superscalaire=== La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. Nous verrons pourquoi dans la suite, mais on peut dire que c'est juste plus simple et plus efficace ainsi. Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop candidate, sous entendu "candidate à l'émission". L'unité d'émission détecte les dépendances entre la µop candidate et les µops en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en cours d'exécution dans le pipeline. mais en plus, elle doit tester les dépendances entre les µops candidates. Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément, il doit détecter les paires d'instructions dépendantes et les sérialiser. Avec une fenêtre d'instruction, il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil gérent les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires modernes utilisent tous une fenêtre d'instruction centralisée ou décentralisée, et non des ''scoreboard''. Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps, là encore en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire peut émettre 8 µops à la fois, on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle. ===L'unité de renommage superscalaire=== Avec la superscalarité, l'unité de renommage de registre doit gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance de registre, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées. Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat. [[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]] Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante. [[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]] Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande. [[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]] ===Le banc de registre d'un processeur superscalaire=== Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement. 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. ==L'évolution historique des processeurs superscalaires== Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs triple émission permettent eux d’émettre trois instructions à la fois. Les tout premiers processeurs superscalaires avaient un budget en transistors très limité, ce qui fait qu'ils ne pouvaient pas être "totalement superscalaires". Par exemple, un processeur double émission ne pouvait pas émettre deux opérations entières en même temps, ni deux opérations flottantes, ni une opération entière avec un accès mémoire. Il y avait des paires d'instructions autorisées et des paires interdites. Les '''contraintes d'appariement''' sur les instructions à émettre en même temps étaient très strictes. L'intérêt de ces contraintes d’appariement est que le processeur était très simple, il n'y avait pas besoin de rajouter des circuits complexes pour rendre le processeur superscalaire. Les CPU superscalaires historiques ne faisaient que mieux utiliser des unités de calcul déjà présentes. Prenons un processeur non-superscalaire simple, illustré ci-dessous. Il contient : une unité de chargement, un décodeur, une unité d'émission, une ALU, une FPU, les bancs de registre entier et flottants, et une unité mémoire. Nous avons placé l'unité mémoire à la suite de l'ALU entière, car ce sera important pour les explications qui vont suivre. [[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]] Les processeurs superscalaires historiques modifiaient assez peu le schéma précédent. L'unité de chargement était adaptée pour lire plusieurs instructions depuis la mémoire, il y avait un seul décodeur, l'unité d'émission était assez peu modifiée. Mais une chose était certaine : aucune unité de calcul n'était dupliquée, le banc de registre n'était presque pas modifié. A la rigueur, gérer la triple 'émission avec les branchements demandait parfois d'ajouter des ports de lecture sur le banc de registre généraux, pas plus. ===La double émission entière-flottante=== Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est d'émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Le processeur a donc deux pipelines séparés : un pour les micro-opérations entières, un autre pour les micro-opérations flottantes. On parle alors de '''double émission entière-flottante'''. La mise en œuvre utilise assez peu de circuits, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité. La plupart des premiers processeurs superscalaires étaient de ce type. Les exemples les plus notables sont les processeurs POWER 1 et ses dérivés comme le ''RISC Single Chip''. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle. [[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]] La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisations visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate]. ===L'émission parallèle des branchements=== Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur. L'idée était d'appliquer un équivalent de la double émission, non pas pour des opérations flottantes, mais pour autre chose. Il y avait deux candidats : l'unité de branchement, l'unité mémoire. Et c'est l'unité de branchement qui a été choisie. Le processeur pouvait émettre un branchement en parallèle d'une instruction entière. La technique s'appelle l''''émission parallèle des branchements'''. Combinée à la double émission entière-flottante, cela permettait d'émettre trois µops en même temps : un branchement, une opération flottante, et une opération entière/mémoire. Les exemples de processeurs de ce type ne sont pas très nombreux, mais on peut citer l'Intel i960CA et l'HyperSPARC. Les branchements étant fréquents, surtout sur les CPU de type RISC, les gains en performances étaient assez impressionnants. Quant au cout en transistor, il dépendait du jeu d’instruction, expliquons pourquoi. Pour les processeurs avec un registre d'état, l'implémentation est particulièrement simple. L'unité de branchement lit ce registre d'état, sélectionne le bit adéquat, teste sa valeur, et modifie le ''program counter'' en fonction. Le processeur intègre déjà une unité de branchement séparée de l'unité de calcul, pour faire tout cela. Pas besoin d'utiliser l'unité de calcul entière, ni toute autre unité. Il n'y a même pas besoin de toucher au banc de registres, vu que le registre d'état n'est pas dedans, idem pour le ''program counter'' Et il en est de même si le processeur utiliser des registres à prédicats. Là encore, il y a un banc de registre séparé pour les prédicats et une unité de branchement dédiée. L'implémentation de la triple émission ne fait alors qu'utiliser au mieux des circuits existants. Sur les CPU de type RISC, sans registre d'état, les choses sont cependant différentes. De tels processeurs ont des instructions de branchements qui lisent deux opérandes dans les registres, les comparent, et altèrent ou non le ''program counter'' suivant le résultat. L'implémentation de la triple émission demande alors d'ajouter deux ports de lecture au banc de registre entier, pour lire les opérandes. Il faut aussi ajouter une unité pour faire la comparaison, si elle n'était pas déjà présente. Certains CPU RISC utilisent une unité de branchement séparée, mais d'autres utilisaient l'ALU entière pour. ===L'émission multiple des micro-opérations entières=== Voyons maintenant l'étape après la triple émission entière-flottante-branchement. Elle est assez simple, à savoir qu'elle permet d'émettre deux instructions entières en même temps, avec éventuellement une instruction flottante en plus. Il est parfois possible de monter à trois ou quatre instructions entières. 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 modifier le banc de registres. Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. [[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]] Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications et les divisions ? Pour étudier ces questions, nous allons prendre le cas d'un processeur capable d’émettre trois opérations en même temps : deux opérations entières, et une flottante, potentiellement d'autres. Le cas des opérations complexes est assez intéressant. Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les ALUs en N exemplaires. Mais ce n'est jamais fait en pratique. Concrètement, seules les ALU entières simples le sont, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ne sont pas dupliqués en N exemplaires, le cout en transistors serait trop important. Pour un processeur à double émission, l'unité d'émission a deux ports d'émission, pour émettre deux µops entières. Les deux ports ont chacun leur ALU entière, mais l'un d'entre eux a est aussi relié au circuit multiplier. En clair, cela permet d'émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Idem pour les opérations simples comme les soustractions, les opérations bit à bit, etc. On peut en exécuter deux, ou en coupler une avec une multiplication. Faire ainsi n'a pas de cout en performance significatif, comparé à une duplication totale des ALUs entières. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur serait inutile. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle. [[File:Emission multiple des opérations entières, implémentation naive.png|centre|vignette|upright=2|Émission multiple des opérations entières, implémentation naïve.]] Il en est de même avec les décalages, vu que le ''barrel shifter'' n'est pas dupliqué. Il est donc possible d'émettre deux opérations de base, un décalage avec une opération de base, mais pas deux décalages en parallèle. En général, le ''barrel shifter'' et le multiplieur sont placés sur deux ports d'émission séparés. Cela permet ainsi de faire un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas. [[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Emission multiple des opérations entières, implémentation courante]] Pour les branchements, deux solutions sont possibles. Avec la première, il y a émission parallèle des branchements, à savoir qu'il est possible d'émettre en branchement en parallèle de deux-trois instructions entières. Et cela impose la présence d'une unité de branchement séparée, avec son port d'émission. Avec la seconde, les branchements sont traités comme des instructions entières. Il peut y avoir une unité de branchement séparée, mais ce n'est pas souvent le cas. En général, les branchements sont exécutés dans une ALU entière. [[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Emission série et parallèle des branchements]] Les accès mémoire sont traités comme des opérations entières un peu spéciales, sur les processeurs superscalaires historiques. En clair, le processeur peut émettre deux opérations entières, ou une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent. Les raisons à cela étaient nombreuses. Et ces raisons sont encore valables sur des petits processeurs destinés à l'embarqué ou l'informatique industrielle. Seuls les processeurs haute performance modernes font différemment. La raison principale est le cout en circuit. Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse. Pas besoin de rajouter une unité de calcul d'adresse séparée. Mais surtout, permettre l'émission parallèle des accès mémoire ferait passer un processeur triple émission à de la quadruple émission, avec tous les couts que ça implique. Il faudrait rajouter des ports de lecture/écriture au banc de registre généraux, pour traiter un accès mémoire en plus du reste. Les circuits d'émission et de chargement devraient être élargit, aussi. Et quitte à payer un tel cout en circuit, il serait plus rentable de l'utiliser pour émettre une quatrième opération entière qu'un accès mémoire. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand-chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2. ===Les CPU superscalaires à plusieurs unités spécialisées=== Le classique deux opérations entières + une flottante était l'idéal pour la triple émission. Même les processeurs modernes à triple émission restent sur cette "norme", car elle est proche de l'optimal. Mais arrivé à la quadruple émission, les possibilités sont devenues plus diverses. L'évolution des CPU superscalaires ont évolués dans des directions différentes, chacun faisant à sa sauce, les choses sont devenues plus confuses. Le seul point commun est qu'ils ont eu tendance à rajouter des unités de calcul. Un point important est que la quadruple émission permet de nombreuses possibilités, mais la plupart ne sont pas intéressantes. Pour donner un exemple concret, prenons un CPU superscalaire très simple, comprenant une ALU entière, une FPU, une unité de branchement, et une unité mémoire. Il est possible de créer un CPU quadruple émission avec ces unités, mais le gain en performance n'en vaudrait pas la peine pour le cout en circuit. Il est rare qu'un bloc de 4 instructions contienne exactement une opération entière, une flottante, un branchement et un accès mémoire. En pratique, les seules combinaisons fréquentes sont les combinaisons calcul + accès mémoire et calcul + branchement. Les combinaisons calcul entier + calcul flottant sont aussi possibles, mais plus rares. Mais on dépasse rarement la double émission. Il est préférable d'utiliser ces unités dans un CPU double ou triple émission, en retirant l'unité de branchement et/ou en couplant l'unité mémoire à l'ALU. En clair, une fois arrivé à la quadruple émission, on ne peut plus se contenter d'une ALU, d'une FPU, d'une unité de branchement et d'une unité mémoire. Les processeurs superscalaires larges n'ont pas le choix que de rajouter des unités de calcul, voire des unités mémoire. Et en pratique, ils ont privilégié l'ajout d'ALU entières, avant de dupliquer les autres circuits. La raison est que les programmes informatiques utilisent beaucoup plus d'additions/soustractions/comparaisons que le reste. Il y a environ 5 opérations entières de base pour une multiplication entière, le ratio est le même quand on remplace la multiplication par un branchement ou un accès mémoire. En conséquence, les ALU entières ont été dupliquées en premier, et dans de nombreux exemplaires. Reste à voir comment a été dupliqué le reste. Les multiplieurs n'ont pas été dupliqués, sauf sur les processeurs superscalaires très larges. La raison est qu'il est rare de devoir exécuter deux multiplications à la fois. C'est la même chose pour le ''barrel shifter''. Deux branchements consécutifs sont quant à eux plus fréquents, c'est même assez courant. Aussi, les unités de branchement ont été dupliquées en second lieu, après les ALU entières. Avoir plusieurs accès mémoire proches est assez courant, surtout sur les CPU de type CISC. Aussi, les unités mémoire ont été dupliquées en troisième lieu. Pour résumer, dupliquer des ALU entières est donc la priorité, suivi par la duplication des unités de branchement, puis celle des accès mémoire, pour terminer par le multiplieur. La FPU est un peu à part. Une autre tendance a été de séparer l'unité mémoire des ALU entières, ce qui a permis l''''émission parallèle des µops mémoire'''. Comme son nom l'indique, l'idée est de séparer les µops mémoire des opérations entières. Il devient alors possible d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière. La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple. La '''double émission flottante''' a été utilisée sur les processeurs Alpha. L'idée est d'émettre deux opérations flottantes à la fois. Sauf qu'une implémentation naïve demanderait de dupliquer la FPU, ce qui aurait un cout en circuit rédhibitoire. Pour rappel, les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant. L'idée est d'avoir des ports d'émission séparés pour l'additionneur flottant et le multiplieur flottant. Le processeur peut alors émettre une addition flottante en même temps qu'une multiplication flottante. Les autres circuits de calcul flottant sont répartis sur ces deux ports d'émission flottants. L'avantage est que cela se marie bien avec l'usage d'une fenêtre d'instruction séparée pour les opérations flottantes. La fenêtre d'instruction a alors deux ports séparés, au lieu d'un seul. Rajouter un second port d'émission flottant n'est pas trop un problème, car le cout lié à l'ajout d'un port n'est pas linéaire. Passer de un port à deux a un cout tolérable, bien plus que de passer de 3 ports à 4 ou de 4 à 5. Il est possible de dupliquer l'additionneur et le multiplieur flottant, ce qui permet d'émettre deux additions et multiplications flottantes en même temps. C'est ce qui est fait sur les processeurs AMD d'architecture Zen 1 et 2. Ils ont deux additionneurs flottants par cœur, deux multiplieurs flottants, chacun avec leur propre port d'émission. Les performances en calcul flottant sont assez impressionnantes pour un processeur de l'époque. [[File:ZEN - émission multiple flottante.png|centre|vignette|upright=2|Microarchitecture Zen 1 d'AMD.]] ==Les ports d'émission et de décodage== Les ports d'émission et de décodage sont des ressources matérielles comme les autres. Dans cette section, nous allons parler de concepts liés aux ports d'émission et de décodage. Nous allons notamment parler du partage des ports d'émission, ainsi que des relations numériques entre ces deux types de ports. ===Le partage des ports d'émission et la ''port contention''=== Il est possible de partager un port d'émission, à savoir de connecter un port d'émission à plusieurs unités de calcul. Nous en avons vu un exemple plus haut, avec l'émission multiple des opérations entières. On peut par exemple connecter un multiplieur et une ALU entière sur le même port d'émission. Pour le moment, nous n'avons vu que des cas où un port d'émission est relié soit à des ALU entières, soit à des FPU, pas aux deux. Mais il existe des processeurs où un même port d'émission alimente à la fois une ALU entière et une FPU. Par exemple, on peut relier un additionneur flottant sur le même port qu'une ALU entière. Il faut noter que cela implique une fenêtre d'instruction centralisée, capable de mettre en attente micro-opérations entières et flottantes. Un exemple est celui des processeurs Core 2 Duo. Ils disposent de 6 ports d'émission, dont 3 ports dédiés à l'unité mémoire. Les 3 ports restants alimentent chacun : une ALU entière, un circuit de calcul flottant et une unité de calcul SSE (une unité de calcul SIMD, qu'on abordera dans quelques chapitres). * Le premier port alimente une ALU entière simple et un multiplieur/diviseur flottant. * Le second alimente une ALU entière, un multiplieur entier et un additionneur flottant. * Le troisième alimente une ALU entière, sans circuit flottant dédié. Dans le même genre, il est possible de partager un port d'émission entre l'unité mémoire et une ALU entière. Cela permet d'utiliser l'ALU entière pour les calculs d'adresse, ce qui évite d'avoir à utiliser une unité de calcul d'adresse distincte. Un exemple est celui du processeur superscalaire double émission Power PC 440. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. L'organisation en question permet soit d'émettre un accès mémoire en même temps qu'une opération entière, soit d'émettre deux opérations entières simples, soit d’émettre une multiplication et une addition/soustraction/comparaison. Une organisation simple, mais très efficace ! [[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]] Regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles, qui ne se présentent que pour certaines combinaisons de micro-opérations bien précises. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port. Le nombre de ports peut être un facteur limitant pour la performance dans certaines situations de '''''port contention''''' où on a assez d'ALU pour exécuter N micro-opérations, mais où la répartition des ALUs sur les ports l’empêche. Mais on peut réduire la ''port contention'' en répartissant correctement les ALU sur les ports d'émission. Le choix en question dépendent fortement du fait qu'en moyenne, que certaines paires d'instructions sont plus fréquentes que d'autres. Tout cela guide ce choix de répartition des ALUs sur les ports. Nous venons de voir que la répartition des ALU sur les ports d'émission est très variable d'un processeur à l'autre. Les divers choix possibles sont tous des compromis entre deux forces : réduire le nombre de ports d'émission d'un côté, garder de bonnes performances en réduisant la ''port contention'' de l'autre. Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction relativement simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ? Par contre, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal réparti les ALU. ===La séparation entre largeur de décodage et d'émission=== Sur les processeurs superscalaires larges, le processeur peut émettre plus d'instructions qu'il peut en décoder. Plus haut, j'ai dit que l'unité d'émission avait parfois plus de ports d'émission que de ports de décodage. La raison est assez simple, et elle s'explique assez bien avec un exemple. Prenons un processeur avec trois ALU entières, une FPU, une unité de branchement et une unité mémoire. Chacune a son propre port d'émission, ce qui fait que le CPU est capable d'émettre 6 µops. Du moins, c'est la théorie, car les conditions pour sont assez rares. Il faut qu'en chargeant un paquet de 6 instructions, il y ait exactement trois instructions entières, une flottante, un branchement et un accès mémoire. Si le paquet contient 5 instructions entières et un branchement, seules quatre instructions sont émises. Dans cet exemple, dans la grande majorité des cas, seuls 4 ports seront utilisés en moyenne et deux seront sous-utilisés. Il s'agit là d'une moyenne, qui dépend du code, et des instructions exactes à exécuter. Cependant, cette moyenne nous donne un moyen d'économiser beaucoup de circuits pour un cout en performance mineur. L'idée est de ne charger que des blocs de 4 instructions, mais d'avoir 6 ports d'émission. Le processeur charge et décode 4 instructions, les quatre instructions sont envoyées sur les ports d'émission adéquat. Le cout en transistors est alors situé entre celui d'un CPU quadruple émission et sextuple émission. Et les performances sont aussi entre les deux. Pour les performances, c'est parce que cela permet d'avoir un CPU quadruple émission avec bien moins de contraintes d’appariement. Imaginez qu'on se soit limité à quatre ports d'émission : il aurait fallu mettre plusieurs unités sur le même port d'émission. En conséquence, il y aurait eu des combinaisons interdites, comme émettre deux instructions entières + un branchement + un accès mémoire ; ou deux instructions entières + une opération flottante + un accès mémoire. De tells appariement interdits sont possibles en utilisant les six ports d'émission. ==L'émission multiple des accès mémoire== Les premiers processeurs superscalaires ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles. Par contre, c'est autre chose sur les processeurs superscalaires larges. Vu qu'ils travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares. L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. La ''load/store queue'' reste une structure unique, mais on peut lui rajouter des ports. Et à ce peit jeu, il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire. La première duplique les unités de calcul, mais pas les ports d'accès au cache. Les accès mémoire sont alors accumulés dans la ''load/store queue'', qui sont ensuite traités un par un par le cache. Un processeur utilisant cette technique est, par exemple, capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. Un exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. Ces processeurs pouvaient calculer trois adresses simultanément, mais ne pouvaient faire qu'un seul accès au cache par cycle. Le cout en circuit est modéré : les unités de calcul coutent très peu, les ports de la ''load/store queue'' coutent déjà un peu plus. La seconde méthode ajoute des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultanées. Il faut alors dupliquer des unités de calcul, rajouter les ports sur le cache, mais aussi rajouter des ports sur la ''load/store queue'', pour qu'elle échange des données/commandes mémoire avec le cache. Le cout en circuits est alors conséquent Quelques processeurs utilisaient des solutions intermédiaires, un exemple classique étant les Pentium 1 et 2 d'Intel. Mais nous en parlerons dans le chapitre suivant. [[File:Processeur non-superscalaire basique 02.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]] Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre. Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Le Pentium 4 était dans ce cas, mais il était un peu seul dans sa catégorie. Un cas similaire est celui où l'unité mémoire est alimentée par une fenêtre de micro-opération mélangeant instructions mémoire et entières. Les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. Dans ce cas, l'émission multiple est rarement implémentée. Le cas suivant a une file pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique. Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui pouvaient seulement émettre une lecture en même temps qu'une écriture. Ils avaient deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse. [[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]] Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture demande d'utiliser une file d'écriture multiport, ce qui est assez compliqué. Implémenter des lectures en plus est plus simple : il suffit de rajouter une seconde file de µops LOAD et une unité de calcul, alimentées via un second port d'émission. Il est aussi possible de rajouter un second port de lecture au cache, mais ce n'est pas obligatoire. Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse. De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports de lecture et d'écriture car il y a plus de lecture que d'écritures. Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. : Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse. [[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]] Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettent plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail. Pour résumer, l'évolution des CPU superscalaires a été la suivante : * pipeline mémorie fusionné avec le pipeline entier ; * séparation du pipeline mémoire du reste, avec émission parallèle des accès mémoire ; * émission multiple, mais sans dupliquer les ports du cache ; * émission multiple, avec émission simultanée d'une lecture et d’une écriture dans le cache ; * émission multiple avec plusieurs accès en lecture au cache. ==Les optimisations des processeurs superscalaires larges== Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul. ===L'étape de chargement d'un CPU superscalaire large=== Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 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 autoaligné.]] Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements ! [[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]] Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez 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 microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total). Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois 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 microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement. Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations. ===La micro-fusion des processeurs Intel et AMD=== La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''. Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''. Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout. La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Le parallélisme mémoire | prevText=Le parallélisme mémoire | next=Exemples de microarchitectures CPU : le cas du x86 | nextText=Exemples de microarchitectures CPU : le cas du x86 }} </noinclude> pr0gjmsswe7p0spoq726uc17gww7ru5 772675 772674 2026-09-21T14:28:41Z Mewtow 31375 /* L'implémentation des processeurs superscalaires */ 772675 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 tout premiers processeurs superscalaires pouvaient émettre deux µops en même temps, ce qui portait le nom de '''double émission'''. Mais le nombre a rapidement augmenté et les processeurs modernes 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. ==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. Par exemple, l'unité de chargement charge deux fois plus d'instructions, mais n'est pas modifiée en profondeur. L'unité d'émission et les circuits d'exécution dans le désordre sont eux aussi altérés. Les unités de calcul sont souvent dupliquées, sauf sur les tout premiers CPU superscalaires. {|class="wikitable" |- ! rowspan="2" | Processeur sans émission multiple | rowspan="2" | Chargement | rowspan="2" | Décodage | rowspan="2" | Renommage | rowspan="2" | Émission | Exécution / ALU | rowspan="2" | ''Commit''/ROB |- | Exécution / ALU |- ! rowspan="4" | Processeur superscalaire | rowspan="4" | Chargement | rowspan="2" | Décodage | rowspan="4" | Renommage | rowspan="4" | Émission | Exécution / ALU | rowspan="4" | ''Commit''/ROB |- | Exécution / ALU |- | rowspan="2" | Décodage | Exécution / ALU |- | Exécution / ALU |} Voyons ce qu'il en est dans le détail. ===Les unités de calcul d'un processeur superscalaire=== Émettre N instructions simultanément demande implicitement d'exécuter N instructions par cycle, voire plus si des instructions multicycles viennent mettre leur grain de sel. Intuitivement, on se dit que les unités de calcul doivent être dupliquées. Et c'est une solution viable pour les processeurs très simples, avec seulement quelques ALU entières. Par exemple, un processeur RISC basique avec seulement une ALU entière et un ''barrel shifter'' peut être rendu superscalaire en dupliquant les deux circuits. Le problème, est que cette solution marche assez mal dès que le processeur contient des circuits multiplieurs et/ou une FPU. Par exemple, un processeur à double émission devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, etc. 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 ! Par contre, l'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission. Un autre cas extrême est celui où aucune unité de calcul n'est dupliquée. Pour comprendre comment c'est possible rappelez-vous qu'un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et éventuellement une unité de branchement. Cela suffit pour créer un processeur quadruple émission, très limité. Un tel processeur peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire. Le problème est qu'il est rare que l'on ait à exécuter en même temps un calcul entier, un calcul flottant, un accès mémoire et un branchement. La quadruple émission n'est donc que rarement utilisée. En pratique, les seules combinaisons fréquentes sont les combinaisons calcul + accès mémoire et calcul + branchement. Les combinaisons calcul entier + calcul flottant sont aussi possibles, mais plus rares. Mais on dépasse rarement la double émission. En clair, à partir de la quadruple émission, on ne peut plus se contenter d'une ALU, d'une FPU, d'une unité de branchement et d'une unité mémoire. Les processeurs superscalaires larges n'ont pas le choix que de rajouter des unités de calcul. Reste à voir ce qui est dupliqué en pratique. 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. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant. La conséquence est que les processeurs superscalaires ont des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a des paires d'instructions autorisées et des paires interdites. Par exemple, l'exécution simultanée de deux branchements est interdite, les branchements sont exécutés l'un après l'autre. Mais il est possible d'émettre un branchement en même temps qu'une autre instruction, en espérant que la prédiction de branchement ait fait une bonne prédiction. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur. En pratique, ces contraintes d’appariement ont peu d’impact. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière, le ratio est le même quand on remplace la multiplication par un branchement ou un accès mémoire. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Deux branchements consécutifs sont quant à eux plus fréquents, idem pour deux accès mémoire. Dupliquer des ALU entières est donc la priorité, suivi par la duplication des unités de branchement, puis celle des accès mémoire, pour terminer par le multiplieur et la FPU. ===Le chargement de plusieurs instructions consécutives=== Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large. Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence. Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas. Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement non-pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc. [[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]] ===Le décodage parallèle des instructions=== Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits. Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur. Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! ===L'unité d'émission d'un processeur superscalaire=== La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. Nous verrons pourquoi dans la suite, mais on peut dire que c'est juste plus simple et plus efficace ainsi. Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop candidate, sous entendu "candidate à l'émission". L'unité d'émission détecte les dépendances entre la µop candidate et les µops en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en cours d'exécution dans le pipeline. mais en plus, elle doit tester les dépendances entre les µops candidates. Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément, il doit détecter les paires d'instructions dépendantes et les sérialiser. Avec une fenêtre d'instruction, il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil gérent les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires modernes utilisent tous une fenêtre d'instruction centralisée ou décentralisée, et non des ''scoreboard''. Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps, là encore en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire peut émettre 8 µops à la fois, on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle. ===L'unité de renommage superscalaire=== Avec la superscalarité, l'unité de renommage de registre doit gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance de registre, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées. Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat. [[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]] Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante. [[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]] Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande. [[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]] ===Le banc de registre d'un processeur superscalaire=== Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement. 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. ==L'évolution historique des processeurs superscalaires== Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs triple émission permettent eux d’émettre trois instructions à la fois. Les tout premiers processeurs superscalaires avaient un budget en transistors très limité, ce qui fait qu'ils ne pouvaient pas être "totalement superscalaires". Par exemple, un processeur double émission ne pouvait pas émettre deux opérations entières en même temps, ni deux opérations flottantes, ni une opération entière avec un accès mémoire. Il y avait des paires d'instructions autorisées et des paires interdites. Les '''contraintes d'appariement''' sur les instructions à émettre en même temps étaient très strictes. L'intérêt de ces contraintes d’appariement est que le processeur était très simple, il n'y avait pas besoin de rajouter des circuits complexes pour rendre le processeur superscalaire. Les CPU superscalaires historiques ne faisaient que mieux utiliser des unités de calcul déjà présentes. Prenons un processeur non-superscalaire simple, illustré ci-dessous. Il contient : une unité de chargement, un décodeur, une unité d'émission, une ALU, une FPU, les bancs de registre entier et flottants, et une unité mémoire. Nous avons placé l'unité mémoire à la suite de l'ALU entière, car ce sera important pour les explications qui vont suivre. [[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]] Les processeurs superscalaires historiques modifiaient assez peu le schéma précédent. L'unité de chargement était adaptée pour lire plusieurs instructions depuis la mémoire, il y avait un seul décodeur, l'unité d'émission était assez peu modifiée. Mais une chose était certaine : aucune unité de calcul n'était dupliquée, le banc de registre n'était presque pas modifié. A la rigueur, gérer la triple 'émission avec les branchements demandait parfois d'ajouter des ports de lecture sur le banc de registre généraux, pas plus. ===La double émission entière-flottante=== Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est d'émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Le processeur a donc deux pipelines séparés : un pour les micro-opérations entières, un autre pour les micro-opérations flottantes. On parle alors de '''double émission entière-flottante'''. La mise en œuvre utilise assez peu de circuits, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité. La plupart des premiers processeurs superscalaires étaient de ce type. Les exemples les plus notables sont les processeurs POWER 1 et ses dérivés comme le ''RISC Single Chip''. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle. [[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]] La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisations visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate]. ===L'émission parallèle des branchements=== Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur. L'idée était d'appliquer un équivalent de la double émission, non pas pour des opérations flottantes, mais pour autre chose. Il y avait deux candidats : l'unité de branchement, l'unité mémoire. Et c'est l'unité de branchement qui a été choisie. Le processeur pouvait émettre un branchement en parallèle d'une instruction entière. La technique s'appelle l''''émission parallèle des branchements'''. Combinée à la double émission entière-flottante, cela permettait d'émettre trois µops en même temps : un branchement, une opération flottante, et une opération entière/mémoire. Les exemples de processeurs de ce type ne sont pas très nombreux, mais on peut citer l'Intel i960CA et l'HyperSPARC. Les branchements étant fréquents, surtout sur les CPU de type RISC, les gains en performances étaient assez impressionnants. Quant au cout en transistor, il dépendait du jeu d’instruction, expliquons pourquoi. Pour les processeurs avec un registre d'état, l'implémentation est particulièrement simple. L'unité de branchement lit ce registre d'état, sélectionne le bit adéquat, teste sa valeur, et modifie le ''program counter'' en fonction. Le processeur intègre déjà une unité de branchement séparée de l'unité de calcul, pour faire tout cela. Pas besoin d'utiliser l'unité de calcul entière, ni toute autre unité. Il n'y a même pas besoin de toucher au banc de registres, vu que le registre d'état n'est pas dedans, idem pour le ''program counter'' Et il en est de même si le processeur utiliser des registres à prédicats. Là encore, il y a un banc de registre séparé pour les prédicats et une unité de branchement dédiée. L'implémentation de la triple émission ne fait alors qu'utiliser au mieux des circuits existants. Sur les CPU de type RISC, sans registre d'état, les choses sont cependant différentes. De tels processeurs ont des instructions de branchements qui lisent deux opérandes dans les registres, les comparent, et altèrent ou non le ''program counter'' suivant le résultat. L'implémentation de la triple émission demande alors d'ajouter deux ports de lecture au banc de registre entier, pour lire les opérandes. Il faut aussi ajouter une unité pour faire la comparaison, si elle n'était pas déjà présente. Certains CPU RISC utilisent une unité de branchement séparée, mais d'autres utilisaient l'ALU entière pour. ===L'émission multiple des micro-opérations entières=== Voyons maintenant l'étape après la triple émission entière-flottante-branchement. Elle est assez simple, à savoir qu'elle permet d'émettre deux instructions entières en même temps, avec éventuellement une instruction flottante en plus. Il est parfois possible de monter à trois ou quatre instructions entières. 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 modifier le banc de registres. Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. [[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]] Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications et les divisions ? Pour étudier ces questions, nous allons prendre le cas d'un processeur capable d’émettre trois opérations en même temps : deux opérations entières, et une flottante, potentiellement d'autres. Le cas des opérations complexes est assez intéressant. Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les ALUs en N exemplaires. Mais ce n'est jamais fait en pratique. Concrètement, seules les ALU entières simples le sont, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ne sont pas dupliqués en N exemplaires, le cout en transistors serait trop important. Pour un processeur à double émission, l'unité d'émission a deux ports d'émission, pour émettre deux µops entières. Les deux ports ont chacun leur ALU entière, mais l'un d'entre eux a est aussi relié au circuit multiplier. En clair, cela permet d'émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Idem pour les opérations simples comme les soustractions, les opérations bit à bit, etc. On peut en exécuter deux, ou en coupler une avec une multiplication. Faire ainsi n'a pas de cout en performance significatif, comparé à une duplication totale des ALUs entières. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur serait inutile. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle. [[File:Emission multiple des opérations entières, implémentation naive.png|centre|vignette|upright=2|Émission multiple des opérations entières, implémentation naïve.]] Il en est de même avec les décalages, vu que le ''barrel shifter'' n'est pas dupliqué. Il est donc possible d'émettre deux opérations de base, un décalage avec une opération de base, mais pas deux décalages en parallèle. En général, le ''barrel shifter'' et le multiplieur sont placés sur deux ports d'émission séparés. Cela permet ainsi de faire un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas. [[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Emission multiple des opérations entières, implémentation courante]] Pour les branchements, deux solutions sont possibles. Avec la première, il y a émission parallèle des branchements, à savoir qu'il est possible d'émettre en branchement en parallèle de deux-trois instructions entières. Et cela impose la présence d'une unité de branchement séparée, avec son port d'émission. Avec la seconde, les branchements sont traités comme des instructions entières. Il peut y avoir une unité de branchement séparée, mais ce n'est pas souvent le cas. En général, les branchements sont exécutés dans une ALU entière. [[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Emission série et parallèle des branchements]] Les accès mémoire sont traités comme des opérations entières un peu spéciales, sur les processeurs superscalaires historiques. En clair, le processeur peut émettre deux opérations entières, ou une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent. Les raisons à cela étaient nombreuses. Et ces raisons sont encore valables sur des petits processeurs destinés à l'embarqué ou l'informatique industrielle. Seuls les processeurs haute performance modernes font différemment. La raison principale est le cout en circuit. Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse. Pas besoin de rajouter une unité de calcul d'adresse séparée. Mais surtout, permettre l'émission parallèle des accès mémoire ferait passer un processeur triple émission à de la quadruple émission, avec tous les couts que ça implique. Il faudrait rajouter des ports de lecture/écriture au banc de registre généraux, pour traiter un accès mémoire en plus du reste. Les circuits d'émission et de chargement devraient être élargit, aussi. Et quitte à payer un tel cout en circuit, il serait plus rentable de l'utiliser pour émettre une quatrième opération entière qu'un accès mémoire. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand-chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2. ===Les CPU superscalaires à plusieurs unités spécialisées=== Le classique deux opérations entières + une flottante était l'idéal pour la triple émission. Même les processeurs modernes à triple émission restent sur cette "norme", car elle est proche de l'optimal. Mais arrivé à la quadruple émission, les possibilités sont devenues plus diverses. L'évolution des CPU superscalaires ont évolués dans des directions différentes, chacun faisant à sa sauce, les choses sont devenues plus confuses. Le seul point commun est qu'ils ont eu tendance à rajouter des unités de calcul. Un point important est que la quadruple émission permet de nombreuses possibilités, mais la plupart ne sont pas intéressantes. Pour donner un exemple concret, prenons un CPU superscalaire très simple, comprenant une ALU entière, une FPU, une unité de branchement, et une unité mémoire. Il est possible de créer un CPU quadruple émission avec ces unités, mais le gain en performance n'en vaudrait pas la peine pour le cout en circuit. Il est rare qu'un bloc de 4 instructions contienne exactement une opération entière, une flottante, un branchement et un accès mémoire. En pratique, les seules combinaisons fréquentes sont les combinaisons calcul + accès mémoire et calcul + branchement. Les combinaisons calcul entier + calcul flottant sont aussi possibles, mais plus rares. Mais on dépasse rarement la double émission. Il est préférable d'utiliser ces unités dans un CPU double ou triple émission, en retirant l'unité de branchement et/ou en couplant l'unité mémoire à l'ALU. En clair, une fois arrivé à la quadruple émission, on ne peut plus se contenter d'une ALU, d'une FPU, d'une unité de branchement et d'une unité mémoire. Les processeurs superscalaires larges n'ont pas le choix que de rajouter des unités de calcul, voire des unités mémoire. Et en pratique, ils ont privilégié l'ajout d'ALU entières, avant de dupliquer les autres circuits. La raison est que les programmes informatiques utilisent beaucoup plus d'additions/soustractions/comparaisons que le reste. Il y a environ 5 opérations entières de base pour une multiplication entière, le ratio est le même quand on remplace la multiplication par un branchement ou un accès mémoire. En conséquence, les ALU entières ont été dupliquées en premier, et dans de nombreux exemplaires. Reste à voir comment a été dupliqué le reste. Les multiplieurs n'ont pas été dupliqués, sauf sur les processeurs superscalaires très larges. La raison est qu'il est rare de devoir exécuter deux multiplications à la fois. C'est la même chose pour le ''barrel shifter''. Deux branchements consécutifs sont quant à eux plus fréquents, c'est même assez courant. Aussi, les unités de branchement ont été dupliquées en second lieu, après les ALU entières. Avoir plusieurs accès mémoire proches est assez courant, surtout sur les CPU de type CISC. Aussi, les unités mémoire ont été dupliquées en troisième lieu. Pour résumer, dupliquer des ALU entières est donc la priorité, suivi par la duplication des unités de branchement, puis celle des accès mémoire, pour terminer par le multiplieur. La FPU est un peu à part. Une autre tendance a été de séparer l'unité mémoire des ALU entières, ce qui a permis l''''émission parallèle des µops mémoire'''. Comme son nom l'indique, l'idée est de séparer les µops mémoire des opérations entières. Il devient alors possible d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière. La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple. La '''double émission flottante''' a été utilisée sur les processeurs Alpha. L'idée est d'émettre deux opérations flottantes à la fois. Sauf qu'une implémentation naïve demanderait de dupliquer la FPU, ce qui aurait un cout en circuit rédhibitoire. Pour rappel, les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant. L'idée est d'avoir des ports d'émission séparés pour l'additionneur flottant et le multiplieur flottant. Le processeur peut alors émettre une addition flottante en même temps qu'une multiplication flottante. Les autres circuits de calcul flottant sont répartis sur ces deux ports d'émission flottants. L'avantage est que cela se marie bien avec l'usage d'une fenêtre d'instruction séparée pour les opérations flottantes. La fenêtre d'instruction a alors deux ports séparés, au lieu d'un seul. Rajouter un second port d'émission flottant n'est pas trop un problème, car le cout lié à l'ajout d'un port n'est pas linéaire. Passer de un port à deux a un cout tolérable, bien plus que de passer de 3 ports à 4 ou de 4 à 5. Il est possible de dupliquer l'additionneur et le multiplieur flottant, ce qui permet d'émettre deux additions et multiplications flottantes en même temps. C'est ce qui est fait sur les processeurs AMD d'architecture Zen 1 et 2. Ils ont deux additionneurs flottants par cœur, deux multiplieurs flottants, chacun avec leur propre port d'émission. Les performances en calcul flottant sont assez impressionnantes pour un processeur de l'époque. [[File:ZEN - émission multiple flottante.png|centre|vignette|upright=2|Microarchitecture Zen 1 d'AMD.]] ==Les ports d'émission et de décodage== Les ports d'émission et de décodage sont des ressources matérielles comme les autres. Dans cette section, nous allons parler de concepts liés aux ports d'émission et de décodage. Nous allons notamment parler du partage des ports d'émission, ainsi que des relations numériques entre ces deux types de ports. ===Le partage des ports d'émission et la ''port contention''=== Il est possible de partager un port d'émission, à savoir de connecter un port d'émission à plusieurs unités de calcul. Nous en avons vu un exemple plus haut, avec l'émission multiple des opérations entières. On peut par exemple connecter un multiplieur et une ALU entière sur le même port d'émission. Pour le moment, nous n'avons vu que des cas où un port d'émission est relié soit à des ALU entières, soit à des FPU, pas aux deux. Mais il existe des processeurs où un même port d'émission alimente à la fois une ALU entière et une FPU. Par exemple, on peut relier un additionneur flottant sur le même port qu'une ALU entière. Il faut noter que cela implique une fenêtre d'instruction centralisée, capable de mettre en attente micro-opérations entières et flottantes. Un exemple est celui des processeurs Core 2 Duo. Ils disposent de 6 ports d'émission, dont 3 ports dédiés à l'unité mémoire. Les 3 ports restants alimentent chacun : une ALU entière, un circuit de calcul flottant et une unité de calcul SSE (une unité de calcul SIMD, qu'on abordera dans quelques chapitres). * Le premier port alimente une ALU entière simple et un multiplieur/diviseur flottant. * Le second alimente une ALU entière, un multiplieur entier et un additionneur flottant. * Le troisième alimente une ALU entière, sans circuit flottant dédié. Dans le même genre, il est possible de partager un port d'émission entre l'unité mémoire et une ALU entière. Cela permet d'utiliser l'ALU entière pour les calculs d'adresse, ce qui évite d'avoir à utiliser une unité de calcul d'adresse distincte. Un exemple est celui du processeur superscalaire double émission Power PC 440. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. L'organisation en question permet soit d'émettre un accès mémoire en même temps qu'une opération entière, soit d'émettre deux opérations entières simples, soit d’émettre une multiplication et une addition/soustraction/comparaison. Une organisation simple, mais très efficace ! [[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]] Regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles, qui ne se présentent que pour certaines combinaisons de micro-opérations bien précises. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port. Le nombre de ports peut être un facteur limitant pour la performance dans certaines situations de '''''port contention''''' où on a assez d'ALU pour exécuter N micro-opérations, mais où la répartition des ALUs sur les ports l’empêche. Mais on peut réduire la ''port contention'' en répartissant correctement les ALU sur les ports d'émission. Le choix en question dépendent fortement du fait qu'en moyenne, que certaines paires d'instructions sont plus fréquentes que d'autres. Tout cela guide ce choix de répartition des ALUs sur les ports. Nous venons de voir que la répartition des ALU sur les ports d'émission est très variable d'un processeur à l'autre. Les divers choix possibles sont tous des compromis entre deux forces : réduire le nombre de ports d'émission d'un côté, garder de bonnes performances en réduisant la ''port contention'' de l'autre. Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction relativement simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ? Par contre, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal réparti les ALU. ===La séparation entre largeur de décodage et d'émission=== Sur les processeurs superscalaires larges, le processeur peut émettre plus d'instructions qu'il peut en décoder. Plus haut, j'ai dit que l'unité d'émission avait parfois plus de ports d'émission que de ports de décodage. La raison est assez simple, et elle s'explique assez bien avec un exemple. Prenons un processeur avec trois ALU entières, une FPU, une unité de branchement et une unité mémoire. Chacune a son propre port d'émission, ce qui fait que le CPU est capable d'émettre 6 µops. Du moins, c'est la théorie, car les conditions pour sont assez rares. Il faut qu'en chargeant un paquet de 6 instructions, il y ait exactement trois instructions entières, une flottante, un branchement et un accès mémoire. Si le paquet contient 5 instructions entières et un branchement, seules quatre instructions sont émises. Dans cet exemple, dans la grande majorité des cas, seuls 4 ports seront utilisés en moyenne et deux seront sous-utilisés. Il s'agit là d'une moyenne, qui dépend du code, et des instructions exactes à exécuter. Cependant, cette moyenne nous donne un moyen d'économiser beaucoup de circuits pour un cout en performance mineur. L'idée est de ne charger que des blocs de 4 instructions, mais d'avoir 6 ports d'émission. Le processeur charge et décode 4 instructions, les quatre instructions sont envoyées sur les ports d'émission adéquat. Le cout en transistors est alors situé entre celui d'un CPU quadruple émission et sextuple émission. Et les performances sont aussi entre les deux. Pour les performances, c'est parce que cela permet d'avoir un CPU quadruple émission avec bien moins de contraintes d’appariement. Imaginez qu'on se soit limité à quatre ports d'émission : il aurait fallu mettre plusieurs unités sur le même port d'émission. En conséquence, il y aurait eu des combinaisons interdites, comme émettre deux instructions entières + un branchement + un accès mémoire ; ou deux instructions entières + une opération flottante + un accès mémoire. De tells appariement interdits sont possibles en utilisant les six ports d'émission. ==L'émission multiple des accès mémoire== Les premiers processeurs superscalaires ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles. Par contre, c'est autre chose sur les processeurs superscalaires larges. Vu qu'ils travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares. L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. La ''load/store queue'' reste une structure unique, mais on peut lui rajouter des ports. Et à ce peit jeu, il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire. La première duplique les unités de calcul, mais pas les ports d'accès au cache. Les accès mémoire sont alors accumulés dans la ''load/store queue'', qui sont ensuite traités un par un par le cache. Un processeur utilisant cette technique est, par exemple, capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. Un exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. Ces processeurs pouvaient calculer trois adresses simultanément, mais ne pouvaient faire qu'un seul accès au cache par cycle. Le cout en circuit est modéré : les unités de calcul coutent très peu, les ports de la ''load/store queue'' coutent déjà un peu plus. La seconde méthode ajoute des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultanées. Il faut alors dupliquer des unités de calcul, rajouter les ports sur le cache, mais aussi rajouter des ports sur la ''load/store queue'', pour qu'elle échange des données/commandes mémoire avec le cache. Le cout en circuits est alors conséquent Quelques processeurs utilisaient des solutions intermédiaires, un exemple classique étant les Pentium 1 et 2 d'Intel. Mais nous en parlerons dans le chapitre suivant. [[File:Processeur non-superscalaire basique 02.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]] Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre. Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Le Pentium 4 était dans ce cas, mais il était un peu seul dans sa catégorie. Un cas similaire est celui où l'unité mémoire est alimentée par une fenêtre de micro-opération mélangeant instructions mémoire et entières. Les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. Dans ce cas, l'émission multiple est rarement implémentée. Le cas suivant a une file pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique. Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui pouvaient seulement émettre une lecture en même temps qu'une écriture. Ils avaient deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse. [[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]] Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture demande d'utiliser une file d'écriture multiport, ce qui est assez compliqué. Implémenter des lectures en plus est plus simple : il suffit de rajouter une seconde file de µops LOAD et une unité de calcul, alimentées via un second port d'émission. Il est aussi possible de rajouter un second port de lecture au cache, mais ce n'est pas obligatoire. Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse. De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports de lecture et d'écriture car il y a plus de lecture que d'écritures. Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. : Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse. [[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]] Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettent plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail. Pour résumer, l'évolution des CPU superscalaires a été la suivante : * pipeline mémorie fusionné avec le pipeline entier ; * séparation du pipeline mémoire du reste, avec émission parallèle des accès mémoire ; * émission multiple, mais sans dupliquer les ports du cache ; * émission multiple, avec émission simultanée d'une lecture et d’une écriture dans le cache ; * émission multiple avec plusieurs accès en lecture au cache. ==Les optimisations des processeurs superscalaires larges== Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul. ===L'étape de chargement d'un CPU superscalaire large=== Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 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 autoaligné.]] Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements ! [[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]] Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez 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 microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total). Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois 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 microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement. Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations. ===La micro-fusion des processeurs Intel et AMD=== La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''. Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''. Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout. La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Le parallélisme mémoire | prevText=Le parallélisme mémoire | next=Exemples de microarchitectures CPU : le cas du x86 | nextText=Exemples de microarchitectures CPU : le cas du x86 }} </noinclude> p9lkzyaqfvg7hlifvjl854yxybs4jm5 772677 772675 2026-09-21T14:32:10Z Mewtow 31375 /* L'émission parallèle des branchements */ 772677 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 tout premiers processeurs superscalaires pouvaient émettre deux µops en même temps, ce qui portait le nom de '''double émission'''. Mais le nombre a rapidement augmenté et les processeurs modernes 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. ==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. Par exemple, l'unité de chargement charge deux fois plus d'instructions, mais n'est pas modifiée en profondeur. L'unité d'émission et les circuits d'exécution dans le désordre sont eux aussi altérés. Les unités de calcul sont souvent dupliquées, sauf sur les tout premiers CPU superscalaires. {|class="wikitable" |- ! rowspan="2" | Processeur sans émission multiple | rowspan="2" | Chargement | rowspan="2" | Décodage | rowspan="2" | Renommage | rowspan="2" | Émission | Exécution / ALU | rowspan="2" | ''Commit''/ROB |- | Exécution / ALU |- ! rowspan="4" | Processeur superscalaire | rowspan="4" | Chargement | rowspan="2" | Décodage | rowspan="4" | Renommage | rowspan="4" | Émission | Exécution / ALU | rowspan="4" | ''Commit''/ROB |- | Exécution / ALU |- | rowspan="2" | Décodage | Exécution / ALU |- | Exécution / ALU |} Voyons ce qu'il en est dans le détail. ===Les unités de calcul d'un processeur superscalaire=== Émettre N instructions simultanément demande implicitement d'exécuter N instructions par cycle, voire plus si des instructions multicycles viennent mettre leur grain de sel. Intuitivement, on se dit que les unités de calcul doivent être dupliquées. Et c'est une solution viable pour les processeurs très simples, avec seulement quelques ALU entières. Par exemple, un processeur RISC basique avec seulement une ALU entière et un ''barrel shifter'' peut être rendu superscalaire en dupliquant les deux circuits. Le problème, est que cette solution marche assez mal dès que le processeur contient des circuits multiplieurs et/ou une FPU. Par exemple, un processeur à double émission devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, etc. 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 ! Par contre, l'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission. Un autre cas extrême est celui où aucune unité de calcul n'est dupliquée. Pour comprendre comment c'est possible rappelez-vous qu'un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et éventuellement une unité de branchement. Cela suffit pour créer un processeur quadruple émission, très limité. Un tel processeur peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire. Le problème est qu'il est rare que l'on ait à exécuter en même temps un calcul entier, un calcul flottant, un accès mémoire et un branchement. La quadruple émission n'est donc que rarement utilisée. En pratique, les seules combinaisons fréquentes sont les combinaisons calcul + accès mémoire et calcul + branchement. Les combinaisons calcul entier + calcul flottant sont aussi possibles, mais plus rares. Mais on dépasse rarement la double émission. En clair, à partir de la quadruple émission, on ne peut plus se contenter d'une ALU, d'une FPU, d'une unité de branchement et d'une unité mémoire. Les processeurs superscalaires larges n'ont pas le choix que de rajouter des unités de calcul. Reste à voir ce qui est dupliqué en pratique. 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. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant. La conséquence est que les processeurs superscalaires ont des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a des paires d'instructions autorisées et des paires interdites. Par exemple, l'exécution simultanée de deux branchements est interdite, les branchements sont exécutés l'un après l'autre. Mais il est possible d'émettre un branchement en même temps qu'une autre instruction, en espérant que la prédiction de branchement ait fait une bonne prédiction. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur. En pratique, ces contraintes d’appariement ont peu d’impact. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière, le ratio est le même quand on remplace la multiplication par un branchement ou un accès mémoire. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Deux branchements consécutifs sont quant à eux plus fréquents, idem pour deux accès mémoire. Dupliquer des ALU entières est donc la priorité, suivi par la duplication des unités de branchement, puis celle des accès mémoire, pour terminer par le multiplieur et la FPU. ===Le chargement de plusieurs instructions consécutives=== Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large. Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence. Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas. Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement non-pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc. [[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]] ===Le décodage parallèle des instructions=== Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits. Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur. Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! ===L'unité d'émission d'un processeur superscalaire=== La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. Nous verrons pourquoi dans la suite, mais on peut dire que c'est juste plus simple et plus efficace ainsi. Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop candidate, sous entendu "candidate à l'émission". L'unité d'émission détecte les dépendances entre la µop candidate et les µops en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en cours d'exécution dans le pipeline. mais en plus, elle doit tester les dépendances entre les µops candidates. Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément, il doit détecter les paires d'instructions dépendantes et les sérialiser. Avec une fenêtre d'instruction, il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil gérent les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires modernes utilisent tous une fenêtre d'instruction centralisée ou décentralisée, et non des ''scoreboard''. Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps, là encore en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire peut émettre 8 µops à la fois, on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle. ===L'unité de renommage superscalaire=== Avec la superscalarité, l'unité de renommage de registre doit gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance de registre, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées. Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat. [[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]] Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante. [[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]] Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande. [[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]] ===Le banc de registre d'un processeur superscalaire=== Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement. 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. ==L'évolution historique des processeurs superscalaires== Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs triple émission permettent eux d’émettre trois instructions à la fois. Les tout premiers processeurs superscalaires avaient un budget en transistors très limité, ce qui fait qu'ils ne pouvaient pas être "totalement superscalaires". Par exemple, un processeur double émission ne pouvait pas émettre deux opérations entières en même temps, ni deux opérations flottantes, ni une opération entière avec un accès mémoire. Il y avait des paires d'instructions autorisées et des paires interdites. Les '''contraintes d'appariement''' sur les instructions à émettre en même temps étaient très strictes. L'intérêt de ces contraintes d’appariement est que le processeur était très simple, il n'y avait pas besoin de rajouter des circuits complexes pour rendre le processeur superscalaire. Les CPU superscalaires historiques ne faisaient que mieux utiliser des unités de calcul déjà présentes. Prenons un processeur non-superscalaire simple, illustré ci-dessous. Il contient : une unité de chargement, un décodeur, une unité d'émission, une ALU, une FPU, les bancs de registre entier et flottants, et une unité mémoire. Nous avons placé l'unité mémoire à la suite de l'ALU entière, car ce sera important pour les explications qui vont suivre. [[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]] Les processeurs superscalaires historiques modifiaient assez peu le schéma précédent. L'unité de chargement était adaptée pour lire plusieurs instructions depuis la mémoire, il y avait un seul décodeur, l'unité d'émission était assez peu modifiée. Mais une chose était certaine : aucune unité de calcul n'était dupliquée, le banc de registre n'était presque pas modifié. A la rigueur, gérer la triple 'émission avec les branchements demandait parfois d'ajouter des ports de lecture sur le banc de registre généraux, pas plus. ===La double émission entière-flottante=== Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est d'émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Le processeur a donc deux pipelines séparés : un pour les micro-opérations entières, un autre pour les micro-opérations flottantes. On parle alors de '''double émission entière-flottante'''. La mise en œuvre utilise assez peu de circuits, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité. La plupart des premiers processeurs superscalaires étaient de ce type. Les exemples les plus notables sont les processeurs POWER 1 et ses dérivés comme le ''RISC Single Chip''. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle. [[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]] La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisations visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate]. ===L'émission parallèle des branchements=== Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur. L'idée était d'appliquer un équivalent de la double émission, non pas pour des opérations flottantes, mais pour autre chose. Il y avait 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. Le processeur pouvait émettre un branchement en parallèle d'une instruction entière. La technique s'appelle l''''émission parallèle des branchements'''. Combinée à la double émission entière-flottante, cela permettait d'émettre trois µops en même temps : un branchement, une opération flottante, et une opération entière/mémoire. Les exemples de processeurs de ce type ne sont pas très nombreux, mais on peut citer l'Intel i960CA et l'HyperSPARC. Les branchements étant fréquents, surtout sur les CPU de type RISC, les gains en performances étaient assez impressionnants. Quant au cout en transistor, il dépendait du jeu d’instruction, expliquons pourquoi. Pour les processeurs avec un registre d'état, l'implémentation est particulièrement simple. L'unité de branchement lit ce registre d'état, sélectionne le bit adéquat, teste sa valeur, et modifie le ''program counter'' en fonction. Le processeur intègre déjà une unité de branchement séparée de l'unité de calcul, pour faire tout cela. Pas besoin d'utiliser l'unité de calcul entière, ni toute autre unité. Il n'y a même pas besoin de toucher au banc de registres, vu que le registre d'état n'est pas dedans, idem pour le ''program counter'' Et il en est de même si le processeur utiliser des registres à prédicats. Là encore, il y a un banc de registre séparé pour les prédicats et une unité de branchement dédiée. L'implémentation de la triple émission ne fait alors qu'utiliser au mieux des circuits existants. Sur les CPU de type RISC, sans registre d'état, les choses sont cependant différentes. De tels processeurs ont des instructions de branchements qui lisent deux opérandes dans les registres, les comparent, et altèrent ou non le ''program counter'' suivant le résultat. L'implémentation de la triple émission demande alors d'ajouter deux ports de lecture au banc de registre entier, pour lire les opérandes. Il faut aussi ajouter une unité pour faire la comparaison, si elle n'était pas déjà présente. Certains CPU RISC utilisent une unité de branchement séparée, mais d'autres utilisaient l'ALU entière pour. ===L'émission multiple des micro-opérations entières=== Voyons maintenant l'étape après la triple émission entière-flottante-branchement. Elle est assez simple, à savoir qu'elle permet d'émettre deux instructions entières en même temps, avec éventuellement une instruction flottante en plus. Il est parfois possible de monter à trois ou quatre instructions entières. 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 modifier le banc de registres. Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. [[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]] Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications et les divisions ? Pour étudier ces questions, nous allons prendre le cas d'un processeur capable d’émettre trois opérations en même temps : deux opérations entières, et une flottante, potentiellement d'autres. Le cas des opérations complexes est assez intéressant. Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les ALUs en N exemplaires. Mais ce n'est jamais fait en pratique. Concrètement, seules les ALU entières simples le sont, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ne sont pas dupliqués en N exemplaires, le cout en transistors serait trop important. Pour un processeur à double émission, l'unité d'émission a deux ports d'émission, pour émettre deux µops entières. Les deux ports ont chacun leur ALU entière, mais l'un d'entre eux a est aussi relié au circuit multiplier. En clair, cela permet d'émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Idem pour les opérations simples comme les soustractions, les opérations bit à bit, etc. On peut en exécuter deux, ou en coupler une avec une multiplication. Faire ainsi n'a pas de cout en performance significatif, comparé à une duplication totale des ALUs entières. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur serait inutile. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle. [[File:Emission multiple des opérations entières, implémentation naive.png|centre|vignette|upright=2|Émission multiple des opérations entières, implémentation naïve.]] Il en est de même avec les décalages, vu que le ''barrel shifter'' n'est pas dupliqué. Il est donc possible d'émettre deux opérations de base, un décalage avec une opération de base, mais pas deux décalages en parallèle. En général, le ''barrel shifter'' et le multiplieur sont placés sur deux ports d'émission séparés. Cela permet ainsi de faire un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas. [[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Emission multiple des opérations entières, implémentation courante]] Pour les branchements, deux solutions sont possibles. Avec la première, il y a émission parallèle des branchements, à savoir qu'il est possible d'émettre en branchement en parallèle de deux-trois instructions entières. Et cela impose la présence d'une unité de branchement séparée, avec son port d'émission. Avec la seconde, les branchements sont traités comme des instructions entières. Il peut y avoir une unité de branchement séparée, mais ce n'est pas souvent le cas. En général, les branchements sont exécutés dans une ALU entière. [[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Emission série et parallèle des branchements]] Les accès mémoire sont traités comme des opérations entières un peu spéciales, sur les processeurs superscalaires historiques. En clair, le processeur peut émettre deux opérations entières, ou une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent. Les raisons à cela étaient nombreuses. Et ces raisons sont encore valables sur des petits processeurs destinés à l'embarqué ou l'informatique industrielle. Seuls les processeurs haute performance modernes font différemment. La raison principale est le cout en circuit. Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse. Pas besoin de rajouter une unité de calcul d'adresse séparée. Mais surtout, permettre l'émission parallèle des accès mémoire ferait passer un processeur triple émission à de la quadruple émission, avec tous les couts que ça implique. Il faudrait rajouter des ports de lecture/écriture au banc de registre généraux, pour traiter un accès mémoire en plus du reste. Les circuits d'émission et de chargement devraient être élargit, aussi. Et quitte à payer un tel cout en circuit, il serait plus rentable de l'utiliser pour émettre une quatrième opération entière qu'un accès mémoire. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand-chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2. ===Les CPU superscalaires à plusieurs unités spécialisées=== Le classique deux opérations entières + une flottante était l'idéal pour la triple émission. Même les processeurs modernes à triple émission restent sur cette "norme", car elle est proche de l'optimal. Mais arrivé à la quadruple émission, les possibilités sont devenues plus diverses. L'évolution des CPU superscalaires ont évolués dans des directions différentes, chacun faisant à sa sauce, les choses sont devenues plus confuses. Le seul point commun est qu'ils ont eu tendance à rajouter des unités de calcul. Un point important est que la quadruple émission permet de nombreuses possibilités, mais la plupart ne sont pas intéressantes. Pour donner un exemple concret, prenons un CPU superscalaire très simple, comprenant une ALU entière, une FPU, une unité de branchement, et une unité mémoire. Il est possible de créer un CPU quadruple émission avec ces unités, mais le gain en performance n'en vaudrait pas la peine pour le cout en circuit. Il est rare qu'un bloc de 4 instructions contienne exactement une opération entière, une flottante, un branchement et un accès mémoire. En pratique, les seules combinaisons fréquentes sont les combinaisons calcul + accès mémoire et calcul + branchement. Les combinaisons calcul entier + calcul flottant sont aussi possibles, mais plus rares. Mais on dépasse rarement la double émission. Il est préférable d'utiliser ces unités dans un CPU double ou triple émission, en retirant l'unité de branchement et/ou en couplant l'unité mémoire à l'ALU. En clair, une fois arrivé à la quadruple émission, on ne peut plus se contenter d'une ALU, d'une FPU, d'une unité de branchement et d'une unité mémoire. Les processeurs superscalaires larges n'ont pas le choix que de rajouter des unités de calcul, voire des unités mémoire. Et en pratique, ils ont privilégié l'ajout d'ALU entières, avant de dupliquer les autres circuits. La raison est que les programmes informatiques utilisent beaucoup plus d'additions/soustractions/comparaisons que le reste. Il y a environ 5 opérations entières de base pour une multiplication entière, le ratio est le même quand on remplace la multiplication par un branchement ou un accès mémoire. En conséquence, les ALU entières ont été dupliquées en premier, et dans de nombreux exemplaires. Reste à voir comment a été dupliqué le reste. Les multiplieurs n'ont pas été dupliqués, sauf sur les processeurs superscalaires très larges. La raison est qu'il est rare de devoir exécuter deux multiplications à la fois. C'est la même chose pour le ''barrel shifter''. Deux branchements consécutifs sont quant à eux plus fréquents, c'est même assez courant. Aussi, les unités de branchement ont été dupliquées en second lieu, après les ALU entières. Avoir plusieurs accès mémoire proches est assez courant, surtout sur les CPU de type CISC. Aussi, les unités mémoire ont été dupliquées en troisième lieu. Pour résumer, dupliquer des ALU entières est donc la priorité, suivi par la duplication des unités de branchement, puis celle des accès mémoire, pour terminer par le multiplieur. La FPU est un peu à part. Une autre tendance a été de séparer l'unité mémoire des ALU entières, ce qui a permis l''''émission parallèle des µops mémoire'''. Comme son nom l'indique, l'idée est de séparer les µops mémoire des opérations entières. Il devient alors possible d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière. La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple. La '''double émission flottante''' a été utilisée sur les processeurs Alpha. L'idée est d'émettre deux opérations flottantes à la fois. Sauf qu'une implémentation naïve demanderait de dupliquer la FPU, ce qui aurait un cout en circuit rédhibitoire. Pour rappel, les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant. L'idée est d'avoir des ports d'émission séparés pour l'additionneur flottant et le multiplieur flottant. Le processeur peut alors émettre une addition flottante en même temps qu'une multiplication flottante. Les autres circuits de calcul flottant sont répartis sur ces deux ports d'émission flottants. L'avantage est que cela se marie bien avec l'usage d'une fenêtre d'instruction séparée pour les opérations flottantes. La fenêtre d'instruction a alors deux ports séparés, au lieu d'un seul. Rajouter un second port d'émission flottant n'est pas trop un problème, car le cout lié à l'ajout d'un port n'est pas linéaire. Passer de un port à deux a un cout tolérable, bien plus que de passer de 3 ports à 4 ou de 4 à 5. Il est possible de dupliquer l'additionneur et le multiplieur flottant, ce qui permet d'émettre deux additions et multiplications flottantes en même temps. C'est ce qui est fait sur les processeurs AMD d'architecture Zen 1 et 2. Ils ont deux additionneurs flottants par cœur, deux multiplieurs flottants, chacun avec leur propre port d'émission. Les performances en calcul flottant sont assez impressionnantes pour un processeur de l'époque. [[File:ZEN - émission multiple flottante.png|centre|vignette|upright=2|Microarchitecture Zen 1 d'AMD.]] ==Les ports d'émission et de décodage== Les ports d'émission et de décodage sont des ressources matérielles comme les autres. Dans cette section, nous allons parler de concepts liés aux ports d'émission et de décodage. Nous allons notamment parler du partage des ports d'émission, ainsi que des relations numériques entre ces deux types de ports. ===Le partage des ports d'émission et la ''port contention''=== Il est possible de partager un port d'émission, à savoir de connecter un port d'émission à plusieurs unités de calcul. Nous en avons vu un exemple plus haut, avec l'émission multiple des opérations entières. On peut par exemple connecter un multiplieur et une ALU entière sur le même port d'émission. Pour le moment, nous n'avons vu que des cas où un port d'émission est relié soit à des ALU entières, soit à des FPU, pas aux deux. Mais il existe des processeurs où un même port d'émission alimente à la fois une ALU entière et une FPU. Par exemple, on peut relier un additionneur flottant sur le même port qu'une ALU entière. Il faut noter que cela implique une fenêtre d'instruction centralisée, capable de mettre en attente micro-opérations entières et flottantes. Un exemple est celui des processeurs Core 2 Duo. Ils disposent de 6 ports d'émission, dont 3 ports dédiés à l'unité mémoire. Les 3 ports restants alimentent chacun : une ALU entière, un circuit de calcul flottant et une unité de calcul SSE (une unité de calcul SIMD, qu'on abordera dans quelques chapitres). * Le premier port alimente une ALU entière simple et un multiplieur/diviseur flottant. * Le second alimente une ALU entière, un multiplieur entier et un additionneur flottant. * Le troisième alimente une ALU entière, sans circuit flottant dédié. Dans le même genre, il est possible de partager un port d'émission entre l'unité mémoire et une ALU entière. Cela permet d'utiliser l'ALU entière pour les calculs d'adresse, ce qui évite d'avoir à utiliser une unité de calcul d'adresse distincte. Un exemple est celui du processeur superscalaire double émission Power PC 440. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. L'organisation en question permet soit d'émettre un accès mémoire en même temps qu'une opération entière, soit d'émettre deux opérations entières simples, soit d’émettre une multiplication et une addition/soustraction/comparaison. Une organisation simple, mais très efficace ! [[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]] Regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles, qui ne se présentent que pour certaines combinaisons de micro-opérations bien précises. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port. Le nombre de ports peut être un facteur limitant pour la performance dans certaines situations de '''''port contention''''' où on a assez d'ALU pour exécuter N micro-opérations, mais où la répartition des ALUs sur les ports l’empêche. Mais on peut réduire la ''port contention'' en répartissant correctement les ALU sur les ports d'émission. Le choix en question dépendent fortement du fait qu'en moyenne, que certaines paires d'instructions sont plus fréquentes que d'autres. Tout cela guide ce choix de répartition des ALUs sur les ports. Nous venons de voir que la répartition des ALU sur les ports d'émission est très variable d'un processeur à l'autre. Les divers choix possibles sont tous des compromis entre deux forces : réduire le nombre de ports d'émission d'un côté, garder de bonnes performances en réduisant la ''port contention'' de l'autre. Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction relativement simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ? Par contre, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal réparti les ALU. ===La séparation entre largeur de décodage et d'émission=== Sur les processeurs superscalaires larges, le processeur peut émettre plus d'instructions qu'il peut en décoder. Plus haut, j'ai dit que l'unité d'émission avait parfois plus de ports d'émission que de ports de décodage. La raison est assez simple, et elle s'explique assez bien avec un exemple. Prenons un processeur avec trois ALU entières, une FPU, une unité de branchement et une unité mémoire. Chacune a son propre port d'émission, ce qui fait que le CPU est capable d'émettre 6 µops. Du moins, c'est la théorie, car les conditions pour sont assez rares. Il faut qu'en chargeant un paquet de 6 instructions, il y ait exactement trois instructions entières, une flottante, un branchement et un accès mémoire. Si le paquet contient 5 instructions entières et un branchement, seules quatre instructions sont émises. Dans cet exemple, dans la grande majorité des cas, seuls 4 ports seront utilisés en moyenne et deux seront sous-utilisés. Il s'agit là d'une moyenne, qui dépend du code, et des instructions exactes à exécuter. Cependant, cette moyenne nous donne un moyen d'économiser beaucoup de circuits pour un cout en performance mineur. L'idée est de ne charger que des blocs de 4 instructions, mais d'avoir 6 ports d'émission. Le processeur charge et décode 4 instructions, les quatre instructions sont envoyées sur les ports d'émission adéquat. Le cout en transistors est alors situé entre celui d'un CPU quadruple émission et sextuple émission. Et les performances sont aussi entre les deux. Pour les performances, c'est parce que cela permet d'avoir un CPU quadruple émission avec bien moins de contraintes d’appariement. Imaginez qu'on se soit limité à quatre ports d'émission : il aurait fallu mettre plusieurs unités sur le même port d'émission. En conséquence, il y aurait eu des combinaisons interdites, comme émettre deux instructions entières + un branchement + un accès mémoire ; ou deux instructions entières + une opération flottante + un accès mémoire. De tells appariement interdits sont possibles en utilisant les six ports d'émission. ==L'émission multiple des accès mémoire== Les premiers processeurs superscalaires ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles. Par contre, c'est autre chose sur les processeurs superscalaires larges. Vu qu'ils travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares. L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. La ''load/store queue'' reste une structure unique, mais on peut lui rajouter des ports. Et à ce peit jeu, il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire. La première duplique les unités de calcul, mais pas les ports d'accès au cache. Les accès mémoire sont alors accumulés dans la ''load/store queue'', qui sont ensuite traités un par un par le cache. Un processeur utilisant cette technique est, par exemple, capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. Un exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. Ces processeurs pouvaient calculer trois adresses simultanément, mais ne pouvaient faire qu'un seul accès au cache par cycle. Le cout en circuit est modéré : les unités de calcul coutent très peu, les ports de la ''load/store queue'' coutent déjà un peu plus. La seconde méthode ajoute des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultanées. Il faut alors dupliquer des unités de calcul, rajouter les ports sur le cache, mais aussi rajouter des ports sur la ''load/store queue'', pour qu'elle échange des données/commandes mémoire avec le cache. Le cout en circuits est alors conséquent Quelques processeurs utilisaient des solutions intermédiaires, un exemple classique étant les Pentium 1 et 2 d'Intel. Mais nous en parlerons dans le chapitre suivant. [[File:Processeur non-superscalaire basique 02.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]] Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre. Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Le Pentium 4 était dans ce cas, mais il était un peu seul dans sa catégorie. Un cas similaire est celui où l'unité mémoire est alimentée par une fenêtre de micro-opération mélangeant instructions mémoire et entières. Les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. Dans ce cas, l'émission multiple est rarement implémentée. Le cas suivant a une file pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique. Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui pouvaient seulement émettre une lecture en même temps qu'une écriture. Ils avaient deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse. [[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]] Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture demande d'utiliser une file d'écriture multiport, ce qui est assez compliqué. Implémenter des lectures en plus est plus simple : il suffit de rajouter une seconde file de µops LOAD et une unité de calcul, alimentées via un second port d'émission. Il est aussi possible de rajouter un second port de lecture au cache, mais ce n'est pas obligatoire. Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse. De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports de lecture et d'écriture car il y a plus de lecture que d'écritures. Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. : Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse. [[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]] Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettent plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail. Pour résumer, l'évolution des CPU superscalaires a été la suivante : * pipeline mémorie fusionné avec le pipeline entier ; * séparation du pipeline mémoire du reste, avec émission parallèle des accès mémoire ; * émission multiple, mais sans dupliquer les ports du cache ; * émission multiple, avec émission simultanée d'une lecture et d’une écriture dans le cache ; * émission multiple avec plusieurs accès en lecture au cache. ==Les optimisations des processeurs superscalaires larges== Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul. ===L'étape de chargement d'un CPU superscalaire large=== Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 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 autoaligné.]] Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements ! [[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]] Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez 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 microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total). Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois 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 microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement. Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations. ===La micro-fusion des processeurs Intel et AMD=== La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''. Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''. Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout. La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Le parallélisme mémoire | prevText=Le parallélisme mémoire | next=Exemples de microarchitectures CPU : le cas du x86 | nextText=Exemples de microarchitectures CPU : le cas du x86 }} </noinclude> ic3y8gmdanidny98c90odsefp42xmxn 772678 772677 2026-09-21T14:33:26Z Mewtow 31375 /* L'émission parallèle des branchements */ 772678 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 tout premiers processeurs superscalaires pouvaient émettre deux µops en même temps, ce qui portait le nom de '''double émission'''. Mais le nombre a rapidement augmenté et les processeurs modernes 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. ==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. Par exemple, l'unité de chargement charge deux fois plus d'instructions, mais n'est pas modifiée en profondeur. L'unité d'émission et les circuits d'exécution dans le désordre sont eux aussi altérés. Les unités de calcul sont souvent dupliquées, sauf sur les tout premiers CPU superscalaires. {|class="wikitable" |- ! rowspan="2" | Processeur sans émission multiple | rowspan="2" | Chargement | rowspan="2" | Décodage | rowspan="2" | Renommage | rowspan="2" | Émission | Exécution / ALU | rowspan="2" | ''Commit''/ROB |- | Exécution / ALU |- ! rowspan="4" | Processeur superscalaire | rowspan="4" | Chargement | rowspan="2" | Décodage | rowspan="4" | Renommage | rowspan="4" | Émission | Exécution / ALU | rowspan="4" | ''Commit''/ROB |- | Exécution / ALU |- | rowspan="2" | Décodage | Exécution / ALU |- | Exécution / ALU |} Voyons ce qu'il en est dans le détail. ===Les unités de calcul d'un processeur superscalaire=== Émettre N instructions simultanément demande implicitement d'exécuter N instructions par cycle, voire plus si des instructions multicycles viennent mettre leur grain de sel. Intuitivement, on se dit que les unités de calcul doivent être dupliquées. Et c'est une solution viable pour les processeurs très simples, avec seulement quelques ALU entières. Par exemple, un processeur RISC basique avec seulement une ALU entière et un ''barrel shifter'' peut être rendu superscalaire en dupliquant les deux circuits. Le problème, est que cette solution marche assez mal dès que le processeur contient des circuits multiplieurs et/ou une FPU. Par exemple, un processeur à double émission devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, etc. 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 ! Par contre, l'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission. Un autre cas extrême est celui où aucune unité de calcul n'est dupliquée. Pour comprendre comment c'est possible rappelez-vous qu'un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et éventuellement une unité de branchement. Cela suffit pour créer un processeur quadruple émission, très limité. Un tel processeur peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire. Le problème est qu'il est rare que l'on ait à exécuter en même temps un calcul entier, un calcul flottant, un accès mémoire et un branchement. La quadruple émission n'est donc que rarement utilisée. En pratique, les seules combinaisons fréquentes sont les combinaisons calcul + accès mémoire et calcul + branchement. Les combinaisons calcul entier + calcul flottant sont aussi possibles, mais plus rares. Mais on dépasse rarement la double émission. En clair, à partir de la quadruple émission, on ne peut plus se contenter d'une ALU, d'une FPU, d'une unité de branchement et d'une unité mémoire. Les processeurs superscalaires larges n'ont pas le choix que de rajouter des unités de calcul. Reste à voir ce qui est dupliqué en pratique. 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. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant. La conséquence est que les processeurs superscalaires ont des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a des paires d'instructions autorisées et des paires interdites. Par exemple, l'exécution simultanée de deux branchements est interdite, les branchements sont exécutés l'un après l'autre. Mais il est possible d'émettre un branchement en même temps qu'une autre instruction, en espérant que la prédiction de branchement ait fait une bonne prédiction. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur. En pratique, ces contraintes d’appariement ont peu d’impact. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière, le ratio est le même quand on remplace la multiplication par un branchement ou un accès mémoire. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Deux branchements consécutifs sont quant à eux plus fréquents, idem pour deux accès mémoire. Dupliquer des ALU entières est donc la priorité, suivi par la duplication des unités de branchement, puis celle des accès mémoire, pour terminer par le multiplieur et la FPU. ===Le chargement de plusieurs instructions consécutives=== Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large. Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence. Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas. Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement non-pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc. [[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]] ===Le décodage parallèle des instructions=== Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits. Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur. Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! ===L'unité d'émission d'un processeur superscalaire=== La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. Nous verrons pourquoi dans la suite, mais on peut dire que c'est juste plus simple et plus efficace ainsi. Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop candidate, sous entendu "candidate à l'émission". L'unité d'émission détecte les dépendances entre la µop candidate et les µops en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en cours d'exécution dans le pipeline. mais en plus, elle doit tester les dépendances entre les µops candidates. Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément, il doit détecter les paires d'instructions dépendantes et les sérialiser. Avec une fenêtre d'instruction, il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil gérent les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires modernes utilisent tous une fenêtre d'instruction centralisée ou décentralisée, et non des ''scoreboard''. Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps, là encore en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire peut émettre 8 µops à la fois, on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle. ===L'unité de renommage superscalaire=== Avec la superscalarité, l'unité de renommage de registre doit gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance de registre, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées. Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat. [[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]] Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante. [[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]] Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande. [[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]] ===Le banc de registre d'un processeur superscalaire=== Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement. 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. ==L'évolution historique des processeurs superscalaires== Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs triple émission permettent eux d’émettre trois instructions à la fois. Les tout premiers processeurs superscalaires avaient un budget en transistors très limité, ce qui fait qu'ils ne pouvaient pas être "totalement superscalaires". Par exemple, un processeur double émission ne pouvait pas émettre deux opérations entières en même temps, ni deux opérations flottantes, ni une opération entière avec un accès mémoire. Il y avait des paires d'instructions autorisées et des paires interdites. Les '''contraintes d'appariement''' sur les instructions à émettre en même temps étaient très strictes. L'intérêt de ces contraintes d’appariement est que le processeur était très simple, il n'y avait pas besoin de rajouter des circuits complexes pour rendre le processeur superscalaire. Les CPU superscalaires historiques ne faisaient que mieux utiliser des unités de calcul déjà présentes. Prenons un processeur non-superscalaire simple, illustré ci-dessous. Il contient : une unité de chargement, un décodeur, une unité d'émission, une ALU, une FPU, les bancs de registre entier et flottants, et une unité mémoire. Nous avons placé l'unité mémoire à la suite de l'ALU entière, car ce sera important pour les explications qui vont suivre. [[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]] Les processeurs superscalaires historiques modifiaient assez peu le schéma précédent. L'unité de chargement était adaptée pour lire plusieurs instructions depuis la mémoire, il y avait un seul décodeur, l'unité d'émission était assez peu modifiée. Mais une chose était certaine : aucune unité de calcul n'était dupliquée, le banc de registre n'était presque pas modifié. A la rigueur, gérer la triple 'émission avec les branchements demandait parfois d'ajouter des ports de lecture sur le banc de registre généraux, pas plus. ===La double émission entière-flottante=== Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est d'émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Le processeur a donc deux pipelines séparés : un pour les micro-opérations entières, un autre pour les micro-opérations flottantes. On parle alors de '''double émission entière-flottante'''. La mise en œuvre utilise assez peu de circuits, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité. La plupart des premiers processeurs superscalaires étaient de ce type. Les exemples les plus notables sont les processeurs POWER 1 et ses dérivés comme le ''RISC Single Chip''. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle. [[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]] La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisations visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate]. ===L'émission parallèle des branchements=== Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur. 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 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 modifier le banc de registres. Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. [[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]] Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications et les divisions ? Pour étudier ces questions, nous allons prendre le cas d'un processeur capable d’émettre trois opérations en même temps : deux opérations entières, et une flottante, potentiellement d'autres. Le cas des opérations complexes est assez intéressant. Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les ALUs en N exemplaires. Mais ce n'est jamais fait en pratique. Concrètement, seules les ALU entières simples le sont, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ne sont pas dupliqués en N exemplaires, le cout en transistors serait trop important. Pour un processeur à double émission, l'unité d'émission a deux ports d'émission, pour émettre deux µops entières. Les deux ports ont chacun leur ALU entière, mais l'un d'entre eux a est aussi relié au circuit multiplier. En clair, cela permet d'émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Idem pour les opérations simples comme les soustractions, les opérations bit à bit, etc. On peut en exécuter deux, ou en coupler une avec une multiplication. Faire ainsi n'a pas de cout en performance significatif, comparé à une duplication totale des ALUs entières. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur serait inutile. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle. [[File:Emission multiple des opérations entières, implémentation naive.png|centre|vignette|upright=2|Émission multiple des opérations entières, implémentation naïve.]] Il en est de même avec les décalages, vu que le ''barrel shifter'' n'est pas dupliqué. Il est donc possible d'émettre deux opérations de base, un décalage avec une opération de base, mais pas deux décalages en parallèle. En général, le ''barrel shifter'' et le multiplieur sont placés sur deux ports d'émission séparés. Cela permet ainsi de faire un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas. [[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Emission multiple des opérations entières, implémentation courante]] Pour les branchements, deux solutions sont possibles. Avec la première, il y a émission parallèle des branchements, à savoir qu'il est possible d'émettre en branchement en parallèle de deux-trois instructions entières. Et cela impose la présence d'une unité de branchement séparée, avec son port d'émission. Avec la seconde, les branchements sont traités comme des instructions entières. Il peut y avoir une unité de branchement séparée, mais ce n'est pas souvent le cas. En général, les branchements sont exécutés dans une ALU entière. [[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Emission série et parallèle des branchements]] Les accès mémoire sont traités comme des opérations entières un peu spéciales, sur les processeurs superscalaires historiques. En clair, le processeur peut émettre deux opérations entières, ou une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent. Les raisons à cela étaient nombreuses. Et ces raisons sont encore valables sur des petits processeurs destinés à l'embarqué ou l'informatique industrielle. Seuls les processeurs haute performance modernes font différemment. La raison principale est le cout en circuit. Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse. Pas besoin de rajouter une unité de calcul d'adresse séparée. Mais surtout, permettre l'émission parallèle des accès mémoire ferait passer un processeur triple émission à de la quadruple émission, avec tous les couts que ça implique. Il faudrait rajouter des ports de lecture/écriture au banc de registre généraux, pour traiter un accès mémoire en plus du reste. Les circuits d'émission et de chargement devraient être élargit, aussi. Et quitte à payer un tel cout en circuit, il serait plus rentable de l'utiliser pour émettre une quatrième opération entière qu'un accès mémoire. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand-chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2. ===Les CPU superscalaires à plusieurs unités spécialisées=== Le classique deux opérations entières + une flottante était l'idéal pour la triple émission. Même les processeurs modernes à triple émission restent sur cette "norme", car elle est proche de l'optimal. Mais arrivé à la quadruple émission, les possibilités sont devenues plus diverses. L'évolution des CPU superscalaires ont évolués dans des directions différentes, chacun faisant à sa sauce, les choses sont devenues plus confuses. Le seul point commun est qu'ils ont eu tendance à rajouter des unités de calcul. Un point important est que la quadruple émission permet de nombreuses possibilités, mais la plupart ne sont pas intéressantes. Pour donner un exemple concret, prenons un CPU superscalaire très simple, comprenant une ALU entière, une FPU, une unité de branchement, et une unité mémoire. Il est possible de créer un CPU quadruple émission avec ces unités, mais le gain en performance n'en vaudrait pas la peine pour le cout en circuit. Il est rare qu'un bloc de 4 instructions contienne exactement une opération entière, une flottante, un branchement et un accès mémoire. En pratique, les seules combinaisons fréquentes sont les combinaisons calcul + accès mémoire et calcul + branchement. Les combinaisons calcul entier + calcul flottant sont aussi possibles, mais plus rares. Mais on dépasse rarement la double émission. Il est préférable d'utiliser ces unités dans un CPU double ou triple émission, en retirant l'unité de branchement et/ou en couplant l'unité mémoire à l'ALU. En clair, une fois arrivé à la quadruple émission, on ne peut plus se contenter d'une ALU, d'une FPU, d'une unité de branchement et d'une unité mémoire. Les processeurs superscalaires larges n'ont pas le choix que de rajouter des unités de calcul, voire des unités mémoire. Et en pratique, ils ont privilégié l'ajout d'ALU entières, avant de dupliquer les autres circuits. La raison est que les programmes informatiques utilisent beaucoup plus d'additions/soustractions/comparaisons que le reste. Il y a environ 5 opérations entières de base pour une multiplication entière, le ratio est le même quand on remplace la multiplication par un branchement ou un accès mémoire. En conséquence, les ALU entières ont été dupliquées en premier, et dans de nombreux exemplaires. Reste à voir comment a été dupliqué le reste. Les multiplieurs n'ont pas été dupliqués, sauf sur les processeurs superscalaires très larges. La raison est qu'il est rare de devoir exécuter deux multiplications à la fois. C'est la même chose pour le ''barrel shifter''. Deux branchements consécutifs sont quant à eux plus fréquents, c'est même assez courant. Aussi, les unités de branchement ont été dupliquées en second lieu, après les ALU entières. Avoir plusieurs accès mémoire proches est assez courant, surtout sur les CPU de type CISC. Aussi, les unités mémoire ont été dupliquées en troisième lieu. Pour résumer, dupliquer des ALU entières est donc la priorité, suivi par la duplication des unités de branchement, puis celle des accès mémoire, pour terminer par le multiplieur. La FPU est un peu à part. Une autre tendance a été de séparer l'unité mémoire des ALU entières, ce qui a permis l''''émission parallèle des µops mémoire'''. Comme son nom l'indique, l'idée est de séparer les µops mémoire des opérations entières. Il devient alors possible d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière. La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple. La '''double émission flottante''' a été utilisée sur les processeurs Alpha. L'idée est d'émettre deux opérations flottantes à la fois. Sauf qu'une implémentation naïve demanderait de dupliquer la FPU, ce qui aurait un cout en circuit rédhibitoire. Pour rappel, les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant. L'idée est d'avoir des ports d'émission séparés pour l'additionneur flottant et le multiplieur flottant. Le processeur peut alors émettre une addition flottante en même temps qu'une multiplication flottante. Les autres circuits de calcul flottant sont répartis sur ces deux ports d'émission flottants. L'avantage est que cela se marie bien avec l'usage d'une fenêtre d'instruction séparée pour les opérations flottantes. La fenêtre d'instruction a alors deux ports séparés, au lieu d'un seul. Rajouter un second port d'émission flottant n'est pas trop un problème, car le cout lié à l'ajout d'un port n'est pas linéaire. Passer de un port à deux a un cout tolérable, bien plus que de passer de 3 ports à 4 ou de 4 à 5. Il est possible de dupliquer l'additionneur et le multiplieur flottant, ce qui permet d'émettre deux additions et multiplications flottantes en même temps. C'est ce qui est fait sur les processeurs AMD d'architecture Zen 1 et 2. Ils ont deux additionneurs flottants par cœur, deux multiplieurs flottants, chacun avec leur propre port d'émission. Les performances en calcul flottant sont assez impressionnantes pour un processeur de l'époque. [[File:ZEN - émission multiple flottante.png|centre|vignette|upright=2|Microarchitecture Zen 1 d'AMD.]] ==Les ports d'émission et de décodage== Les ports d'émission et de décodage sont des ressources matérielles comme les autres. Dans cette section, nous allons parler de concepts liés aux ports d'émission et de décodage. Nous allons notamment parler du partage des ports d'émission, ainsi que des relations numériques entre ces deux types de ports. ===Le partage des ports d'émission et la ''port contention''=== Il est possible de partager un port d'émission, à savoir de connecter un port d'émission à plusieurs unités de calcul. Nous en avons vu un exemple plus haut, avec l'émission multiple des opérations entières. On peut par exemple connecter un multiplieur et une ALU entière sur le même port d'émission. Pour le moment, nous n'avons vu que des cas où un port d'émission est relié soit à des ALU entières, soit à des FPU, pas aux deux. Mais il existe des processeurs où un même port d'émission alimente à la fois une ALU entière et une FPU. Par exemple, on peut relier un additionneur flottant sur le même port qu'une ALU entière. Il faut noter que cela implique une fenêtre d'instruction centralisée, capable de mettre en attente micro-opérations entières et flottantes. Un exemple est celui des processeurs Core 2 Duo. Ils disposent de 6 ports d'émission, dont 3 ports dédiés à l'unité mémoire. Les 3 ports restants alimentent chacun : une ALU entière, un circuit de calcul flottant et une unité de calcul SSE (une unité de calcul SIMD, qu'on abordera dans quelques chapitres). * Le premier port alimente une ALU entière simple et un multiplieur/diviseur flottant. * Le second alimente une ALU entière, un multiplieur entier et un additionneur flottant. * Le troisième alimente une ALU entière, sans circuit flottant dédié. Dans le même genre, il est possible de partager un port d'émission entre l'unité mémoire et une ALU entière. Cela permet d'utiliser l'ALU entière pour les calculs d'adresse, ce qui évite d'avoir à utiliser une unité de calcul d'adresse distincte. Un exemple est celui du processeur superscalaire double émission Power PC 440. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. L'organisation en question permet soit d'émettre un accès mémoire en même temps qu'une opération entière, soit d'émettre deux opérations entières simples, soit d’émettre une multiplication et une addition/soustraction/comparaison. Une organisation simple, mais très efficace ! [[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]] Regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles, qui ne se présentent que pour certaines combinaisons de micro-opérations bien précises. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port. Le nombre de ports peut être un facteur limitant pour la performance dans certaines situations de '''''port contention''''' où on a assez d'ALU pour exécuter N micro-opérations, mais où la répartition des ALUs sur les ports l’empêche. Mais on peut réduire la ''port contention'' en répartissant correctement les ALU sur les ports d'émission. Le choix en question dépendent fortement du fait qu'en moyenne, que certaines paires d'instructions sont plus fréquentes que d'autres. Tout cela guide ce choix de répartition des ALUs sur les ports. Nous venons de voir que la répartition des ALU sur les ports d'émission est très variable d'un processeur à l'autre. Les divers choix possibles sont tous des compromis entre deux forces : réduire le nombre de ports d'émission d'un côté, garder de bonnes performances en réduisant la ''port contention'' de l'autre. Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction relativement simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ? Par contre, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal réparti les ALU. ===La séparation entre largeur de décodage et d'émission=== Sur les processeurs superscalaires larges, le processeur peut émettre plus d'instructions qu'il peut en décoder. Plus haut, j'ai dit que l'unité d'émission avait parfois plus de ports d'émission que de ports de décodage. La raison est assez simple, et elle s'explique assez bien avec un exemple. Prenons un processeur avec trois ALU entières, une FPU, une unité de branchement et une unité mémoire. Chacune a son propre port d'émission, ce qui fait que le CPU est capable d'émettre 6 µops. Du moins, c'est la théorie, car les conditions pour sont assez rares. Il faut qu'en chargeant un paquet de 6 instructions, il y ait exactement trois instructions entières, une flottante, un branchement et un accès mémoire. Si le paquet contient 5 instructions entières et un branchement, seules quatre instructions sont émises. Dans cet exemple, dans la grande majorité des cas, seuls 4 ports seront utilisés en moyenne et deux seront sous-utilisés. Il s'agit là d'une moyenne, qui dépend du code, et des instructions exactes à exécuter. Cependant, cette moyenne nous donne un moyen d'économiser beaucoup de circuits pour un cout en performance mineur. L'idée est de ne charger que des blocs de 4 instructions, mais d'avoir 6 ports d'émission. Le processeur charge et décode 4 instructions, les quatre instructions sont envoyées sur les ports d'émission adéquat. Le cout en transistors est alors situé entre celui d'un CPU quadruple émission et sextuple émission. Et les performances sont aussi entre les deux. Pour les performances, c'est parce que cela permet d'avoir un CPU quadruple émission avec bien moins de contraintes d’appariement. Imaginez qu'on se soit limité à quatre ports d'émission : il aurait fallu mettre plusieurs unités sur le même port d'émission. En conséquence, il y aurait eu des combinaisons interdites, comme émettre deux instructions entières + un branchement + un accès mémoire ; ou deux instructions entières + une opération flottante + un accès mémoire. De tells appariement interdits sont possibles en utilisant les six ports d'émission. ==L'émission multiple des accès mémoire== Les premiers processeurs superscalaires ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles. Par contre, c'est autre chose sur les processeurs superscalaires larges. Vu qu'ils travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares. L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. La ''load/store queue'' reste une structure unique, mais on peut lui rajouter des ports. Et à ce peit jeu, il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire. La première duplique les unités de calcul, mais pas les ports d'accès au cache. Les accès mémoire sont alors accumulés dans la ''load/store queue'', qui sont ensuite traités un par un par le cache. Un processeur utilisant cette technique est, par exemple, capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. Un exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. Ces processeurs pouvaient calculer trois adresses simultanément, mais ne pouvaient faire qu'un seul accès au cache par cycle. Le cout en circuit est modéré : les unités de calcul coutent très peu, les ports de la ''load/store queue'' coutent déjà un peu plus. La seconde méthode ajoute des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultanées. Il faut alors dupliquer des unités de calcul, rajouter les ports sur le cache, mais aussi rajouter des ports sur la ''load/store queue'', pour qu'elle échange des données/commandes mémoire avec le cache. Le cout en circuits est alors conséquent Quelques processeurs utilisaient des solutions intermédiaires, un exemple classique étant les Pentium 1 et 2 d'Intel. Mais nous en parlerons dans le chapitre suivant. [[File:Processeur non-superscalaire basique 02.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]] Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre. Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Le Pentium 4 était dans ce cas, mais il était un peu seul dans sa catégorie. Un cas similaire est celui où l'unité mémoire est alimentée par une fenêtre de micro-opération mélangeant instructions mémoire et entières. Les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. Dans ce cas, l'émission multiple est rarement implémentée. Le cas suivant a une file pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique. Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui pouvaient seulement émettre une lecture en même temps qu'une écriture. Ils avaient deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse. [[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]] Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture demande d'utiliser une file d'écriture multiport, ce qui est assez compliqué. Implémenter des lectures en plus est plus simple : il suffit de rajouter une seconde file de µops LOAD et une unité de calcul, alimentées via un second port d'émission. Il est aussi possible de rajouter un second port de lecture au cache, mais ce n'est pas obligatoire. Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse. De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports de lecture et d'écriture car il y a plus de lecture que d'écritures. Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. : Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse. [[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]] Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettent plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail. Pour résumer, l'évolution des CPU superscalaires a été la suivante : * pipeline mémorie fusionné avec le pipeline entier ; * séparation du pipeline mémoire du reste, avec émission parallèle des accès mémoire ; * émission multiple, mais sans dupliquer les ports du cache ; * émission multiple, avec émission simultanée d'une lecture et d’une écriture dans le cache ; * émission multiple avec plusieurs accès en lecture au cache. ==Les optimisations des processeurs superscalaires larges== Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul. ===L'étape de chargement d'un CPU superscalaire large=== Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 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 autoaligné.]] Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements ! [[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]] Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez 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 microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total). Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois 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 microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement. Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations. ===La micro-fusion des processeurs Intel et AMD=== La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''. Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''. Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout. La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Le parallélisme mémoire | prevText=Le parallélisme mémoire | next=Exemples de microarchitectures CPU : le cas du x86 | nextText=Exemples de microarchitectures CPU : le cas du x86 }} </noinclude> jl1sr0joqwwsf3jx73u362w92a4btrh 772679 772678 2026-09-21T14:35:09Z Mewtow 31375 /* L'émission parallèle des branchements */ 772679 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 tout premiers processeurs superscalaires pouvaient émettre deux µops en même temps, ce qui portait le nom de '''double émission'''. Mais le nombre a rapidement augmenté et les processeurs modernes 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. ==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. Par exemple, l'unité de chargement charge deux fois plus d'instructions, mais n'est pas modifiée en profondeur. L'unité d'émission et les circuits d'exécution dans le désordre sont eux aussi altérés. Les unités de calcul sont souvent dupliquées, sauf sur les tout premiers CPU superscalaires. {|class="wikitable" |- ! rowspan="2" | Processeur sans émission multiple | rowspan="2" | Chargement | rowspan="2" | Décodage | rowspan="2" | Renommage | rowspan="2" | Émission | Exécution / ALU | rowspan="2" | ''Commit''/ROB |- | Exécution / ALU |- ! rowspan="4" | Processeur superscalaire | rowspan="4" | Chargement | rowspan="2" | Décodage | rowspan="4" | Renommage | rowspan="4" | Émission | Exécution / ALU | rowspan="4" | ''Commit''/ROB |- | Exécution / ALU |- | rowspan="2" | Décodage | Exécution / ALU |- | Exécution / ALU |} Voyons ce qu'il en est dans le détail. ===Les unités de calcul d'un processeur superscalaire=== Émettre N instructions simultanément demande implicitement d'exécuter N instructions par cycle, voire plus si des instructions multicycles viennent mettre leur grain de sel. Intuitivement, on se dit que les unités de calcul doivent être dupliquées. Et c'est une solution viable pour les processeurs très simples, avec seulement quelques ALU entières. Par exemple, un processeur RISC basique avec seulement une ALU entière et un ''barrel shifter'' peut être rendu superscalaire en dupliquant les deux circuits. Le problème, est que cette solution marche assez mal dès que le processeur contient des circuits multiplieurs et/ou une FPU. Par exemple, un processeur à double émission devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, etc. 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 ! Par contre, l'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission. Un autre cas extrême est celui où aucune unité de calcul n'est dupliquée. Pour comprendre comment c'est possible rappelez-vous qu'un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et éventuellement une unité de branchement. Cela suffit pour créer un processeur quadruple émission, très limité. Un tel processeur peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire. Le problème est qu'il est rare que l'on ait à exécuter en même temps un calcul entier, un calcul flottant, un accès mémoire et un branchement. La quadruple émission n'est donc que rarement utilisée. En pratique, les seules combinaisons fréquentes sont les combinaisons calcul + accès mémoire et calcul + branchement. Les combinaisons calcul entier + calcul flottant sont aussi possibles, mais plus rares. Mais on dépasse rarement la double émission. En clair, à partir de la quadruple émission, on ne peut plus se contenter d'une ALU, d'une FPU, d'une unité de branchement et d'une unité mémoire. Les processeurs superscalaires larges n'ont pas le choix que de rajouter des unités de calcul. Reste à voir ce qui est dupliqué en pratique. 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. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant. La conséquence est que les processeurs superscalaires ont des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a des paires d'instructions autorisées et des paires interdites. Par exemple, l'exécution simultanée de deux branchements est interdite, les branchements sont exécutés l'un après l'autre. Mais il est possible d'émettre un branchement en même temps qu'une autre instruction, en espérant que la prédiction de branchement ait fait une bonne prédiction. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur. En pratique, ces contraintes d’appariement ont peu d’impact. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière, le ratio est le même quand on remplace la multiplication par un branchement ou un accès mémoire. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Deux branchements consécutifs sont quant à eux plus fréquents, idem pour deux accès mémoire. Dupliquer des ALU entières est donc la priorité, suivi par la duplication des unités de branchement, puis celle des accès mémoire, pour terminer par le multiplieur et la FPU. ===Le chargement de plusieurs instructions consécutives=== Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large. Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence. Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas. Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement non-pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc. [[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]] ===Le décodage parallèle des instructions=== Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits. Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur. Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! ===L'unité d'émission d'un processeur superscalaire=== La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. Nous verrons pourquoi dans la suite, mais on peut dire que c'est juste plus simple et plus efficace ainsi. Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop candidate, sous entendu "candidate à l'émission". L'unité d'émission détecte les dépendances entre la µop candidate et les µops en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en cours d'exécution dans le pipeline. mais en plus, elle doit tester les dépendances entre les µops candidates. Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément, il doit détecter les paires d'instructions dépendantes et les sérialiser. Avec une fenêtre d'instruction, il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil gérent les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires modernes utilisent tous une fenêtre d'instruction centralisée ou décentralisée, et non des ''scoreboard''. Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps, là encore en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire peut émettre 8 µops à la fois, on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle. ===L'unité de renommage superscalaire=== Avec la superscalarité, l'unité de renommage de registre doit gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance de registre, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées. Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat. [[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]] Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante. [[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]] Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande. [[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]] ===Le banc de registre d'un processeur superscalaire=== Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement. 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. ==L'évolution historique des processeurs superscalaires== Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs triple émission permettent eux d’émettre trois instructions à la fois. Les tout premiers processeurs superscalaires avaient un budget en transistors très limité, ce qui fait qu'ils ne pouvaient pas être "totalement superscalaires". Par exemple, un processeur double émission ne pouvait pas émettre deux opérations entières en même temps, ni deux opérations flottantes, ni une opération entière avec un accès mémoire. Il y avait des paires d'instructions autorisées et des paires interdites. Les '''contraintes d'appariement''' sur les instructions à émettre en même temps étaient très strictes. L'intérêt de ces contraintes d’appariement est que le processeur était très simple, il n'y avait pas besoin de rajouter des circuits complexes pour rendre le processeur superscalaire. Les CPU superscalaires historiques ne faisaient que mieux utiliser des unités de calcul déjà présentes. Prenons un processeur non-superscalaire simple, illustré ci-dessous. Il contient : une unité de chargement, un décodeur, une unité d'émission, une ALU, une FPU, les bancs de registre entier et flottants, et une unité mémoire. Nous avons placé l'unité mémoire à la suite de l'ALU entière, car ce sera important pour les explications qui vont suivre. [[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]] Les processeurs superscalaires historiques modifiaient assez peu le schéma précédent. L'unité de chargement était adaptée pour lire plusieurs instructions depuis la mémoire, il y avait un seul décodeur, l'unité d'émission était assez peu modifiée. Mais une chose était certaine : aucune unité de calcul n'était dupliquée, le banc de registre n'était presque pas modifié. A la rigueur, gérer la triple 'émission avec les branchements demandait parfois d'ajouter des ports de lecture sur le banc de registre généraux, pas plus. ===La double émission entière-flottante=== Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est d'émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Le processeur a donc deux pipelines séparés : un pour les micro-opérations entières, un autre pour les micro-opérations flottantes. On parle alors de '''double émission entière-flottante'''. La mise en œuvre utilise assez peu de circuits, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité. La plupart des premiers processeurs superscalaires étaient de ce type. Les exemples les plus notables sont les processeurs POWER 1 et ses dérivés comme le ''RISC Single Chip''. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle. [[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]] La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisations visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate]. ===L'émission parallèle des branchements=== Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur. 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 |} 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 modifier le banc de registres. Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. [[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]] Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications et les divisions ? Pour étudier ces questions, nous allons prendre le cas d'un processeur capable d’émettre trois opérations en même temps : deux opérations entières, et une flottante, potentiellement d'autres. Le cas des opérations complexes est assez intéressant. Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les ALUs en N exemplaires. Mais ce n'est jamais fait en pratique. Concrètement, seules les ALU entières simples le sont, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ne sont pas dupliqués en N exemplaires, le cout en transistors serait trop important. Pour un processeur à double émission, l'unité d'émission a deux ports d'émission, pour émettre deux µops entières. Les deux ports ont chacun leur ALU entière, mais l'un d'entre eux a est aussi relié au circuit multiplier. En clair, cela permet d'émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Idem pour les opérations simples comme les soustractions, les opérations bit à bit, etc. On peut en exécuter deux, ou en coupler une avec une multiplication. Faire ainsi n'a pas de cout en performance significatif, comparé à une duplication totale des ALUs entières. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur serait inutile. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle. [[File:Emission multiple des opérations entières, implémentation naive.png|centre|vignette|upright=2|Émission multiple des opérations entières, implémentation naïve.]] Il en est de même avec les décalages, vu que le ''barrel shifter'' n'est pas dupliqué. Il est donc possible d'émettre deux opérations de base, un décalage avec une opération de base, mais pas deux décalages en parallèle. En général, le ''barrel shifter'' et le multiplieur sont placés sur deux ports d'émission séparés. Cela permet ainsi de faire un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas. [[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Emission multiple des opérations entières, implémentation courante]] Pour les branchements, deux solutions sont possibles. Avec la première, il y a émission parallèle des branchements, à savoir qu'il est possible d'émettre en branchement en parallèle de deux-trois instructions entières. Et cela impose la présence d'une unité de branchement séparée, avec son port d'émission. Avec la seconde, les branchements sont traités comme des instructions entières. Il peut y avoir une unité de branchement séparée, mais ce n'est pas souvent le cas. En général, les branchements sont exécutés dans une ALU entière. [[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Emission série et parallèle des branchements]] Les accès mémoire sont traités comme des opérations entières un peu spéciales, sur les processeurs superscalaires historiques. En clair, le processeur peut émettre deux opérations entières, ou une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent. Les raisons à cela étaient nombreuses. Et ces raisons sont encore valables sur des petits processeurs destinés à l'embarqué ou l'informatique industrielle. Seuls les processeurs haute performance modernes font différemment. La raison principale est le cout en circuit. Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse. Pas besoin de rajouter une unité de calcul d'adresse séparée. Mais surtout, permettre l'émission parallèle des accès mémoire ferait passer un processeur triple émission à de la quadruple émission, avec tous les couts que ça implique. Il faudrait rajouter des ports de lecture/écriture au banc de registre généraux, pour traiter un accès mémoire en plus du reste. Les circuits d'émission et de chargement devraient être élargit, aussi. Et quitte à payer un tel cout en circuit, il serait plus rentable de l'utiliser pour émettre une quatrième opération entière qu'un accès mémoire. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand-chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2. ===Les CPU superscalaires à plusieurs unités spécialisées=== Le classique deux opérations entières + une flottante était l'idéal pour la triple émission. Même les processeurs modernes à triple émission restent sur cette "norme", car elle est proche de l'optimal. Mais arrivé à la quadruple émission, les possibilités sont devenues plus diverses. L'évolution des CPU superscalaires ont évolués dans des directions différentes, chacun faisant à sa sauce, les choses sont devenues plus confuses. Le seul point commun est qu'ils ont eu tendance à rajouter des unités de calcul. Un point important est que la quadruple émission permet de nombreuses possibilités, mais la plupart ne sont pas intéressantes. Pour donner un exemple concret, prenons un CPU superscalaire très simple, comprenant une ALU entière, une FPU, une unité de branchement, et une unité mémoire. Il est possible de créer un CPU quadruple émission avec ces unités, mais le gain en performance n'en vaudrait pas la peine pour le cout en circuit. Il est rare qu'un bloc de 4 instructions contienne exactement une opération entière, une flottante, un branchement et un accès mémoire. En pratique, les seules combinaisons fréquentes sont les combinaisons calcul + accès mémoire et calcul + branchement. Les combinaisons calcul entier + calcul flottant sont aussi possibles, mais plus rares. Mais on dépasse rarement la double émission. Il est préférable d'utiliser ces unités dans un CPU double ou triple émission, en retirant l'unité de branchement et/ou en couplant l'unité mémoire à l'ALU. En clair, une fois arrivé à la quadruple émission, on ne peut plus se contenter d'une ALU, d'une FPU, d'une unité de branchement et d'une unité mémoire. Les processeurs superscalaires larges n'ont pas le choix que de rajouter des unités de calcul, voire des unités mémoire. Et en pratique, ils ont privilégié l'ajout d'ALU entières, avant de dupliquer les autres circuits. La raison est que les programmes informatiques utilisent beaucoup plus d'additions/soustractions/comparaisons que le reste. Il y a environ 5 opérations entières de base pour une multiplication entière, le ratio est le même quand on remplace la multiplication par un branchement ou un accès mémoire. En conséquence, les ALU entières ont été dupliquées en premier, et dans de nombreux exemplaires. Reste à voir comment a été dupliqué le reste. Les multiplieurs n'ont pas été dupliqués, sauf sur les processeurs superscalaires très larges. La raison est qu'il est rare de devoir exécuter deux multiplications à la fois. C'est la même chose pour le ''barrel shifter''. Deux branchements consécutifs sont quant à eux plus fréquents, c'est même assez courant. Aussi, les unités de branchement ont été dupliquées en second lieu, après les ALU entières. Avoir plusieurs accès mémoire proches est assez courant, surtout sur les CPU de type CISC. Aussi, les unités mémoire ont été dupliquées en troisième lieu. Pour résumer, dupliquer des ALU entières est donc la priorité, suivi par la duplication des unités de branchement, puis celle des accès mémoire, pour terminer par le multiplieur. La FPU est un peu à part. Une autre tendance a été de séparer l'unité mémoire des ALU entières, ce qui a permis l''''émission parallèle des µops mémoire'''. Comme son nom l'indique, l'idée est de séparer les µops mémoire des opérations entières. Il devient alors possible d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière. La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple. La '''double émission flottante''' a été utilisée sur les processeurs Alpha. L'idée est d'émettre deux opérations flottantes à la fois. Sauf qu'une implémentation naïve demanderait de dupliquer la FPU, ce qui aurait un cout en circuit rédhibitoire. Pour rappel, les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant. L'idée est d'avoir des ports d'émission séparés pour l'additionneur flottant et le multiplieur flottant. Le processeur peut alors émettre une addition flottante en même temps qu'une multiplication flottante. Les autres circuits de calcul flottant sont répartis sur ces deux ports d'émission flottants. L'avantage est que cela se marie bien avec l'usage d'une fenêtre d'instruction séparée pour les opérations flottantes. La fenêtre d'instruction a alors deux ports séparés, au lieu d'un seul. Rajouter un second port d'émission flottant n'est pas trop un problème, car le cout lié à l'ajout d'un port n'est pas linéaire. Passer de un port à deux a un cout tolérable, bien plus que de passer de 3 ports à 4 ou de 4 à 5. Il est possible de dupliquer l'additionneur et le multiplieur flottant, ce qui permet d'émettre deux additions et multiplications flottantes en même temps. C'est ce qui est fait sur les processeurs AMD d'architecture Zen 1 et 2. Ils ont deux additionneurs flottants par cœur, deux multiplieurs flottants, chacun avec leur propre port d'émission. Les performances en calcul flottant sont assez impressionnantes pour un processeur de l'époque. [[File:ZEN - émission multiple flottante.png|centre|vignette|upright=2|Microarchitecture Zen 1 d'AMD.]] ==Les ports d'émission et de décodage== Les ports d'émission et de décodage sont des ressources matérielles comme les autres. Dans cette section, nous allons parler de concepts liés aux ports d'émission et de décodage. Nous allons notamment parler du partage des ports d'émission, ainsi que des relations numériques entre ces deux types de ports. ===Le partage des ports d'émission et la ''port contention''=== Il est possible de partager un port d'émission, à savoir de connecter un port d'émission à plusieurs unités de calcul. Nous en avons vu un exemple plus haut, avec l'émission multiple des opérations entières. On peut par exemple connecter un multiplieur et une ALU entière sur le même port d'émission. Pour le moment, nous n'avons vu que des cas où un port d'émission est relié soit à des ALU entières, soit à des FPU, pas aux deux. Mais il existe des processeurs où un même port d'émission alimente à la fois une ALU entière et une FPU. Par exemple, on peut relier un additionneur flottant sur le même port qu'une ALU entière. Il faut noter que cela implique une fenêtre d'instruction centralisée, capable de mettre en attente micro-opérations entières et flottantes. Un exemple est celui des processeurs Core 2 Duo. Ils disposent de 6 ports d'émission, dont 3 ports dédiés à l'unité mémoire. Les 3 ports restants alimentent chacun : une ALU entière, un circuit de calcul flottant et une unité de calcul SSE (une unité de calcul SIMD, qu'on abordera dans quelques chapitres). * Le premier port alimente une ALU entière simple et un multiplieur/diviseur flottant. * Le second alimente une ALU entière, un multiplieur entier et un additionneur flottant. * Le troisième alimente une ALU entière, sans circuit flottant dédié. Dans le même genre, il est possible de partager un port d'émission entre l'unité mémoire et une ALU entière. Cela permet d'utiliser l'ALU entière pour les calculs d'adresse, ce qui évite d'avoir à utiliser une unité de calcul d'adresse distincte. Un exemple est celui du processeur superscalaire double émission Power PC 440. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. L'organisation en question permet soit d'émettre un accès mémoire en même temps qu'une opération entière, soit d'émettre deux opérations entières simples, soit d’émettre une multiplication et une addition/soustraction/comparaison. Une organisation simple, mais très efficace ! [[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]] Regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles, qui ne se présentent que pour certaines combinaisons de micro-opérations bien précises. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port. Le nombre de ports peut être un facteur limitant pour la performance dans certaines situations de '''''port contention''''' où on a assez d'ALU pour exécuter N micro-opérations, mais où la répartition des ALUs sur les ports l’empêche. Mais on peut réduire la ''port contention'' en répartissant correctement les ALU sur les ports d'émission. Le choix en question dépendent fortement du fait qu'en moyenne, que certaines paires d'instructions sont plus fréquentes que d'autres. Tout cela guide ce choix de répartition des ALUs sur les ports. Nous venons de voir que la répartition des ALU sur les ports d'émission est très variable d'un processeur à l'autre. Les divers choix possibles sont tous des compromis entre deux forces : réduire le nombre de ports d'émission d'un côté, garder de bonnes performances en réduisant la ''port contention'' de l'autre. Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction relativement simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ? Par contre, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal réparti les ALU. ===La séparation entre largeur de décodage et d'émission=== Sur les processeurs superscalaires larges, le processeur peut émettre plus d'instructions qu'il peut en décoder. Plus haut, j'ai dit que l'unité d'émission avait parfois plus de ports d'émission que de ports de décodage. La raison est assez simple, et elle s'explique assez bien avec un exemple. Prenons un processeur avec trois ALU entières, une FPU, une unité de branchement et une unité mémoire. Chacune a son propre port d'émission, ce qui fait que le CPU est capable d'émettre 6 µops. Du moins, c'est la théorie, car les conditions pour sont assez rares. Il faut qu'en chargeant un paquet de 6 instructions, il y ait exactement trois instructions entières, une flottante, un branchement et un accès mémoire. Si le paquet contient 5 instructions entières et un branchement, seules quatre instructions sont émises. Dans cet exemple, dans la grande majorité des cas, seuls 4 ports seront utilisés en moyenne et deux seront sous-utilisés. Il s'agit là d'une moyenne, qui dépend du code, et des instructions exactes à exécuter. Cependant, cette moyenne nous donne un moyen d'économiser beaucoup de circuits pour un cout en performance mineur. L'idée est de ne charger que des blocs de 4 instructions, mais d'avoir 6 ports d'émission. Le processeur charge et décode 4 instructions, les quatre instructions sont envoyées sur les ports d'émission adéquat. Le cout en transistors est alors situé entre celui d'un CPU quadruple émission et sextuple émission. Et les performances sont aussi entre les deux. Pour les performances, c'est parce que cela permet d'avoir un CPU quadruple émission avec bien moins de contraintes d’appariement. Imaginez qu'on se soit limité à quatre ports d'émission : il aurait fallu mettre plusieurs unités sur le même port d'émission. En conséquence, il y aurait eu des combinaisons interdites, comme émettre deux instructions entières + un branchement + un accès mémoire ; ou deux instructions entières + une opération flottante + un accès mémoire. De tells appariement interdits sont possibles en utilisant les six ports d'émission. ==L'émission multiple des accès mémoire== Les premiers processeurs superscalaires ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles. Par contre, c'est autre chose sur les processeurs superscalaires larges. Vu qu'ils travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares. L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. La ''load/store queue'' reste une structure unique, mais on peut lui rajouter des ports. Et à ce peit jeu, il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire. La première duplique les unités de calcul, mais pas les ports d'accès au cache. Les accès mémoire sont alors accumulés dans la ''load/store queue'', qui sont ensuite traités un par un par le cache. Un processeur utilisant cette technique est, par exemple, capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. Un exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. Ces processeurs pouvaient calculer trois adresses simultanément, mais ne pouvaient faire qu'un seul accès au cache par cycle. Le cout en circuit est modéré : les unités de calcul coutent très peu, les ports de la ''load/store queue'' coutent déjà un peu plus. La seconde méthode ajoute des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultanées. Il faut alors dupliquer des unités de calcul, rajouter les ports sur le cache, mais aussi rajouter des ports sur la ''load/store queue'', pour qu'elle échange des données/commandes mémoire avec le cache. Le cout en circuits est alors conséquent Quelques processeurs utilisaient des solutions intermédiaires, un exemple classique étant les Pentium 1 et 2 d'Intel. Mais nous en parlerons dans le chapitre suivant. [[File:Processeur non-superscalaire basique 02.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]] Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre. Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Le Pentium 4 était dans ce cas, mais il était un peu seul dans sa catégorie. Un cas similaire est celui où l'unité mémoire est alimentée par une fenêtre de micro-opération mélangeant instructions mémoire et entières. Les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. Dans ce cas, l'émission multiple est rarement implémentée. Le cas suivant a une file pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique. Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui pouvaient seulement émettre une lecture en même temps qu'une écriture. Ils avaient deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse. [[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]] Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture demande d'utiliser une file d'écriture multiport, ce qui est assez compliqué. Implémenter des lectures en plus est plus simple : il suffit de rajouter une seconde file de µops LOAD et une unité de calcul, alimentées via un second port d'émission. Il est aussi possible de rajouter un second port de lecture au cache, mais ce n'est pas obligatoire. Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse. De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports de lecture et d'écriture car il y a plus de lecture que d'écritures. Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. : Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse. [[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]] Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettent plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail. Pour résumer, l'évolution des CPU superscalaires a été la suivante : * pipeline mémorie fusionné avec le pipeline entier ; * séparation du pipeline mémoire du reste, avec émission parallèle des accès mémoire ; * émission multiple, mais sans dupliquer les ports du cache ; * émission multiple, avec émission simultanée d'une lecture et d’une écriture dans le cache ; * émission multiple avec plusieurs accès en lecture au cache. ==Les optimisations des processeurs superscalaires larges== Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul. ===L'étape de chargement d'un CPU superscalaire large=== Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 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 autoaligné.]] Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements ! [[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]] Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez 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 microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total). Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois 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 microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement. Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations. ===La micro-fusion des processeurs Intel et AMD=== La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''. Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''. Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout. La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Le parallélisme mémoire | prevText=Le parallélisme mémoire | next=Exemples de microarchitectures CPU : le cas du x86 | nextText=Exemples de microarchitectures CPU : le cas du x86 }} </noinclude> 2mzkt865luyj5jvchwqxzn7cjsmruwv 772680 772679 2026-09-21T14:35:22Z Mewtow 31375 /* La double émission entière-flottante */ 772680 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 tout premiers processeurs superscalaires pouvaient émettre deux µops en même temps, ce qui portait le nom de '''double émission'''. Mais le nombre a rapidement augmenté et les processeurs modernes 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. ==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. Par exemple, l'unité de chargement charge deux fois plus d'instructions, mais n'est pas modifiée en profondeur. L'unité d'émission et les circuits d'exécution dans le désordre sont eux aussi altérés. Les unités de calcul sont souvent dupliquées, sauf sur les tout premiers CPU superscalaires. {|class="wikitable" |- ! rowspan="2" | Processeur sans émission multiple | rowspan="2" | Chargement | rowspan="2" | Décodage | rowspan="2" | Renommage | rowspan="2" | Émission | Exécution / ALU | rowspan="2" | ''Commit''/ROB |- | Exécution / ALU |- ! rowspan="4" | Processeur superscalaire | rowspan="4" | Chargement | rowspan="2" | Décodage | rowspan="4" | Renommage | rowspan="4" | Émission | Exécution / ALU | rowspan="4" | ''Commit''/ROB |- | Exécution / ALU |- | rowspan="2" | Décodage | Exécution / ALU |- | Exécution / ALU |} Voyons ce qu'il en est dans le détail. ===Les unités de calcul d'un processeur superscalaire=== Émettre N instructions simultanément demande implicitement d'exécuter N instructions par cycle, voire plus si des instructions multicycles viennent mettre leur grain de sel. Intuitivement, on se dit que les unités de calcul doivent être dupliquées. Et c'est une solution viable pour les processeurs très simples, avec seulement quelques ALU entières. Par exemple, un processeur RISC basique avec seulement une ALU entière et un ''barrel shifter'' peut être rendu superscalaire en dupliquant les deux circuits. Le problème, est que cette solution marche assez mal dès que le processeur contient des circuits multiplieurs et/ou une FPU. Par exemple, un processeur à double émission devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, etc. 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 ! Par contre, l'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission. Un autre cas extrême est celui où aucune unité de calcul n'est dupliquée. Pour comprendre comment c'est possible rappelez-vous qu'un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et éventuellement une unité de branchement. Cela suffit pour créer un processeur quadruple émission, très limité. Un tel processeur peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire. Le problème est qu'il est rare que l'on ait à exécuter en même temps un calcul entier, un calcul flottant, un accès mémoire et un branchement. La quadruple émission n'est donc que rarement utilisée. En pratique, les seules combinaisons fréquentes sont les combinaisons calcul + accès mémoire et calcul + branchement. Les combinaisons calcul entier + calcul flottant sont aussi possibles, mais plus rares. Mais on dépasse rarement la double émission. En clair, à partir de la quadruple émission, on ne peut plus se contenter d'une ALU, d'une FPU, d'une unité de branchement et d'une unité mémoire. Les processeurs superscalaires larges n'ont pas le choix que de rajouter des unités de calcul. Reste à voir ce qui est dupliqué en pratique. 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. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant. La conséquence est que les processeurs superscalaires ont des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a des paires d'instructions autorisées et des paires interdites. Par exemple, l'exécution simultanée de deux branchements est interdite, les branchements sont exécutés l'un après l'autre. Mais il est possible d'émettre un branchement en même temps qu'une autre instruction, en espérant que la prédiction de branchement ait fait une bonne prédiction. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur. En pratique, ces contraintes d’appariement ont peu d’impact. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière, le ratio est le même quand on remplace la multiplication par un branchement ou un accès mémoire. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Deux branchements consécutifs sont quant à eux plus fréquents, idem pour deux accès mémoire. Dupliquer des ALU entières est donc la priorité, suivi par la duplication des unités de branchement, puis celle des accès mémoire, pour terminer par le multiplieur et la FPU. ===Le chargement de plusieurs instructions consécutives=== Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large. Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence. Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas. Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement non-pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc. [[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]] ===Le décodage parallèle des instructions=== Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits. Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur. Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! ===L'unité d'émission d'un processeur superscalaire=== La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. Nous verrons pourquoi dans la suite, mais on peut dire que c'est juste plus simple et plus efficace ainsi. Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop candidate, sous entendu "candidate à l'émission". L'unité d'émission détecte les dépendances entre la µop candidate et les µops en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en cours d'exécution dans le pipeline. mais en plus, elle doit tester les dépendances entre les µops candidates. Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément, il doit détecter les paires d'instructions dépendantes et les sérialiser. Avec une fenêtre d'instruction, il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil gérent les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires modernes utilisent tous une fenêtre d'instruction centralisée ou décentralisée, et non des ''scoreboard''. Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps, là encore en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire peut émettre 8 µops à la fois, on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle. ===L'unité de renommage superscalaire=== Avec la superscalarité, l'unité de renommage de registre doit gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance de registre, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées. Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat. [[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]] Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante. [[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]] Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande. [[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]] ===Le banc de registre d'un processeur superscalaire=== Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement. 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. ==L'évolution historique des processeurs superscalaires== Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs triple émission permettent eux d’émettre trois instructions à la fois. Les tout premiers processeurs superscalaires avaient un budget en transistors très limité, ce qui fait qu'ils ne pouvaient pas être "totalement superscalaires". Par exemple, un processeur double émission ne pouvait pas émettre deux opérations entières en même temps, ni deux opérations flottantes, ni une opération entière avec un accès mémoire. Il y avait des paires d'instructions autorisées et des paires interdites. Les '''contraintes d'appariement''' sur les instructions à émettre en même temps étaient très strictes. L'intérêt de ces contraintes d’appariement est que le processeur était très simple, il n'y avait pas besoin de rajouter des circuits complexes pour rendre le processeur superscalaire. Les CPU superscalaires historiques ne faisaient que mieux utiliser des unités de calcul déjà présentes. Prenons un processeur non-superscalaire simple, illustré ci-dessous. Il contient : une unité de chargement, un décodeur, une unité d'émission, une ALU, une FPU, les bancs de registre entier et flottants, et une unité mémoire. Nous avons placé l'unité mémoire à la suite de l'ALU entière, car ce sera important pour les explications qui vont suivre. [[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]] Les processeurs superscalaires historiques modifiaient assez peu le schéma précédent. L'unité de chargement était adaptée pour lire plusieurs instructions depuis la mémoire, il y avait un seul décodeur, l'unité d'émission était assez peu modifiée. Mais une chose était certaine : aucune unité de calcul n'était dupliquée, le banc de registre n'était presque pas modifié. A la rigueur, gérer la triple 'émission avec les branchements demandait parfois d'ajouter des ports de lecture sur le banc de registre généraux, pas plus. ===La double émission entière-flottante=== Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est d'émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Le processeur a donc deux pipelines séparés : un pour les micro-opérations entières, un autre pour les micro-opérations flottantes. On parle alors de '''double émission entière-flottante'''. {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 |- | Opération entière ||Opération flottante |} La mise en œuvre utilise assez peu de circuits, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité. La plupart des premiers processeurs superscalaires étaient de ce type. Les exemples les plus notables sont les processeurs POWER 1 et ses dérivés comme le ''RISC Single Chip''. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle. [[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]] La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisations visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate]. ===L'émission parallèle des branchements=== Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur. 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 |} 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 modifier le banc de registres. Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. [[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]] Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications et les divisions ? Pour étudier ces questions, nous allons prendre le cas d'un processeur capable d’émettre trois opérations en même temps : deux opérations entières, et une flottante, potentiellement d'autres. Le cas des opérations complexes est assez intéressant. Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les ALUs en N exemplaires. Mais ce n'est jamais fait en pratique. Concrètement, seules les ALU entières simples le sont, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ne sont pas dupliqués en N exemplaires, le cout en transistors serait trop important. Pour un processeur à double émission, l'unité d'émission a deux ports d'émission, pour émettre deux µops entières. Les deux ports ont chacun leur ALU entière, mais l'un d'entre eux a est aussi relié au circuit multiplier. En clair, cela permet d'émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Idem pour les opérations simples comme les soustractions, les opérations bit à bit, etc. On peut en exécuter deux, ou en coupler une avec une multiplication. Faire ainsi n'a pas de cout en performance significatif, comparé à une duplication totale des ALUs entières. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur serait inutile. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle. [[File:Emission multiple des opérations entières, implémentation naive.png|centre|vignette|upright=2|Émission multiple des opérations entières, implémentation naïve.]] Il en est de même avec les décalages, vu que le ''barrel shifter'' n'est pas dupliqué. Il est donc possible d'émettre deux opérations de base, un décalage avec une opération de base, mais pas deux décalages en parallèle. En général, le ''barrel shifter'' et le multiplieur sont placés sur deux ports d'émission séparés. Cela permet ainsi de faire un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas. [[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Emission multiple des opérations entières, implémentation courante]] Pour les branchements, deux solutions sont possibles. Avec la première, il y a émission parallèle des branchements, à savoir qu'il est possible d'émettre en branchement en parallèle de deux-trois instructions entières. Et cela impose la présence d'une unité de branchement séparée, avec son port d'émission. Avec la seconde, les branchements sont traités comme des instructions entières. Il peut y avoir une unité de branchement séparée, mais ce n'est pas souvent le cas. En général, les branchements sont exécutés dans une ALU entière. [[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Emission série et parallèle des branchements]] Les accès mémoire sont traités comme des opérations entières un peu spéciales, sur les processeurs superscalaires historiques. En clair, le processeur peut émettre deux opérations entières, ou une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent. Les raisons à cela étaient nombreuses. Et ces raisons sont encore valables sur des petits processeurs destinés à l'embarqué ou l'informatique industrielle. Seuls les processeurs haute performance modernes font différemment. La raison principale est le cout en circuit. Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse. Pas besoin de rajouter une unité de calcul d'adresse séparée. Mais surtout, permettre l'émission parallèle des accès mémoire ferait passer un processeur triple émission à de la quadruple émission, avec tous les couts que ça implique. Il faudrait rajouter des ports de lecture/écriture au banc de registre généraux, pour traiter un accès mémoire en plus du reste. Les circuits d'émission et de chargement devraient être élargit, aussi. Et quitte à payer un tel cout en circuit, il serait plus rentable de l'utiliser pour émettre une quatrième opération entière qu'un accès mémoire. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand-chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2. ===Les CPU superscalaires à plusieurs unités spécialisées=== Le classique deux opérations entières + une flottante était l'idéal pour la triple émission. Même les processeurs modernes à triple émission restent sur cette "norme", car elle est proche de l'optimal. Mais arrivé à la quadruple émission, les possibilités sont devenues plus diverses. L'évolution des CPU superscalaires ont évolués dans des directions différentes, chacun faisant à sa sauce, les choses sont devenues plus confuses. Le seul point commun est qu'ils ont eu tendance à rajouter des unités de calcul. Un point important est que la quadruple émission permet de nombreuses possibilités, mais la plupart ne sont pas intéressantes. Pour donner un exemple concret, prenons un CPU superscalaire très simple, comprenant une ALU entière, une FPU, une unité de branchement, et une unité mémoire. Il est possible de créer un CPU quadruple émission avec ces unités, mais le gain en performance n'en vaudrait pas la peine pour le cout en circuit. Il est rare qu'un bloc de 4 instructions contienne exactement une opération entière, une flottante, un branchement et un accès mémoire. En pratique, les seules combinaisons fréquentes sont les combinaisons calcul + accès mémoire et calcul + branchement. Les combinaisons calcul entier + calcul flottant sont aussi possibles, mais plus rares. Mais on dépasse rarement la double émission. Il est préférable d'utiliser ces unités dans un CPU double ou triple émission, en retirant l'unité de branchement et/ou en couplant l'unité mémoire à l'ALU. En clair, une fois arrivé à la quadruple émission, on ne peut plus se contenter d'une ALU, d'une FPU, d'une unité de branchement et d'une unité mémoire. Les processeurs superscalaires larges n'ont pas le choix que de rajouter des unités de calcul, voire des unités mémoire. Et en pratique, ils ont privilégié l'ajout d'ALU entières, avant de dupliquer les autres circuits. La raison est que les programmes informatiques utilisent beaucoup plus d'additions/soustractions/comparaisons que le reste. Il y a environ 5 opérations entières de base pour une multiplication entière, le ratio est le même quand on remplace la multiplication par un branchement ou un accès mémoire. En conséquence, les ALU entières ont été dupliquées en premier, et dans de nombreux exemplaires. Reste à voir comment a été dupliqué le reste. Les multiplieurs n'ont pas été dupliqués, sauf sur les processeurs superscalaires très larges. La raison est qu'il est rare de devoir exécuter deux multiplications à la fois. C'est la même chose pour le ''barrel shifter''. Deux branchements consécutifs sont quant à eux plus fréquents, c'est même assez courant. Aussi, les unités de branchement ont été dupliquées en second lieu, après les ALU entières. Avoir plusieurs accès mémoire proches est assez courant, surtout sur les CPU de type CISC. Aussi, les unités mémoire ont été dupliquées en troisième lieu. Pour résumer, dupliquer des ALU entières est donc la priorité, suivi par la duplication des unités de branchement, puis celle des accès mémoire, pour terminer par le multiplieur. La FPU est un peu à part. Une autre tendance a été de séparer l'unité mémoire des ALU entières, ce qui a permis l''''émission parallèle des µops mémoire'''. Comme son nom l'indique, l'idée est de séparer les µops mémoire des opérations entières. Il devient alors possible d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière. La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple. La '''double émission flottante''' a été utilisée sur les processeurs Alpha. L'idée est d'émettre deux opérations flottantes à la fois. Sauf qu'une implémentation naïve demanderait de dupliquer la FPU, ce qui aurait un cout en circuit rédhibitoire. Pour rappel, les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant. L'idée est d'avoir des ports d'émission séparés pour l'additionneur flottant et le multiplieur flottant. Le processeur peut alors émettre une addition flottante en même temps qu'une multiplication flottante. Les autres circuits de calcul flottant sont répartis sur ces deux ports d'émission flottants. L'avantage est que cela se marie bien avec l'usage d'une fenêtre d'instruction séparée pour les opérations flottantes. La fenêtre d'instruction a alors deux ports séparés, au lieu d'un seul. Rajouter un second port d'émission flottant n'est pas trop un problème, car le cout lié à l'ajout d'un port n'est pas linéaire. Passer de un port à deux a un cout tolérable, bien plus que de passer de 3 ports à 4 ou de 4 à 5. Il est possible de dupliquer l'additionneur et le multiplieur flottant, ce qui permet d'émettre deux additions et multiplications flottantes en même temps. C'est ce qui est fait sur les processeurs AMD d'architecture Zen 1 et 2. Ils ont deux additionneurs flottants par cœur, deux multiplieurs flottants, chacun avec leur propre port d'émission. Les performances en calcul flottant sont assez impressionnantes pour un processeur de l'époque. [[File:ZEN - émission multiple flottante.png|centre|vignette|upright=2|Microarchitecture Zen 1 d'AMD.]] ==Les ports d'émission et de décodage== Les ports d'émission et de décodage sont des ressources matérielles comme les autres. Dans cette section, nous allons parler de concepts liés aux ports d'émission et de décodage. Nous allons notamment parler du partage des ports d'émission, ainsi que des relations numériques entre ces deux types de ports. ===Le partage des ports d'émission et la ''port contention''=== Il est possible de partager un port d'émission, à savoir de connecter un port d'émission à plusieurs unités de calcul. Nous en avons vu un exemple plus haut, avec l'émission multiple des opérations entières. On peut par exemple connecter un multiplieur et une ALU entière sur le même port d'émission. Pour le moment, nous n'avons vu que des cas où un port d'émission est relié soit à des ALU entières, soit à des FPU, pas aux deux. Mais il existe des processeurs où un même port d'émission alimente à la fois une ALU entière et une FPU. Par exemple, on peut relier un additionneur flottant sur le même port qu'une ALU entière. Il faut noter que cela implique une fenêtre d'instruction centralisée, capable de mettre en attente micro-opérations entières et flottantes. Un exemple est celui des processeurs Core 2 Duo. Ils disposent de 6 ports d'émission, dont 3 ports dédiés à l'unité mémoire. Les 3 ports restants alimentent chacun : une ALU entière, un circuit de calcul flottant et une unité de calcul SSE (une unité de calcul SIMD, qu'on abordera dans quelques chapitres). * Le premier port alimente une ALU entière simple et un multiplieur/diviseur flottant. * Le second alimente une ALU entière, un multiplieur entier et un additionneur flottant. * Le troisième alimente une ALU entière, sans circuit flottant dédié. Dans le même genre, il est possible de partager un port d'émission entre l'unité mémoire et une ALU entière. Cela permet d'utiliser l'ALU entière pour les calculs d'adresse, ce qui évite d'avoir à utiliser une unité de calcul d'adresse distincte. Un exemple est celui du processeur superscalaire double émission Power PC 440. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. L'organisation en question permet soit d'émettre un accès mémoire en même temps qu'une opération entière, soit d'émettre deux opérations entières simples, soit d’émettre une multiplication et une addition/soustraction/comparaison. Une organisation simple, mais très efficace ! [[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]] Regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles, qui ne se présentent que pour certaines combinaisons de micro-opérations bien précises. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port. Le nombre de ports peut être un facteur limitant pour la performance dans certaines situations de '''''port contention''''' où on a assez d'ALU pour exécuter N micro-opérations, mais où la répartition des ALUs sur les ports l’empêche. Mais on peut réduire la ''port contention'' en répartissant correctement les ALU sur les ports d'émission. Le choix en question dépendent fortement du fait qu'en moyenne, que certaines paires d'instructions sont plus fréquentes que d'autres. Tout cela guide ce choix de répartition des ALUs sur les ports. Nous venons de voir que la répartition des ALU sur les ports d'émission est très variable d'un processeur à l'autre. Les divers choix possibles sont tous des compromis entre deux forces : réduire le nombre de ports d'émission d'un côté, garder de bonnes performances en réduisant la ''port contention'' de l'autre. Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction relativement simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ? Par contre, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal réparti les ALU. ===La séparation entre largeur de décodage et d'émission=== Sur les processeurs superscalaires larges, le processeur peut émettre plus d'instructions qu'il peut en décoder. Plus haut, j'ai dit que l'unité d'émission avait parfois plus de ports d'émission que de ports de décodage. La raison est assez simple, et elle s'explique assez bien avec un exemple. Prenons un processeur avec trois ALU entières, une FPU, une unité de branchement et une unité mémoire. Chacune a son propre port d'émission, ce qui fait que le CPU est capable d'émettre 6 µops. Du moins, c'est la théorie, car les conditions pour sont assez rares. Il faut qu'en chargeant un paquet de 6 instructions, il y ait exactement trois instructions entières, une flottante, un branchement et un accès mémoire. Si le paquet contient 5 instructions entières et un branchement, seules quatre instructions sont émises. Dans cet exemple, dans la grande majorité des cas, seuls 4 ports seront utilisés en moyenne et deux seront sous-utilisés. Il s'agit là d'une moyenne, qui dépend du code, et des instructions exactes à exécuter. Cependant, cette moyenne nous donne un moyen d'économiser beaucoup de circuits pour un cout en performance mineur. L'idée est de ne charger que des blocs de 4 instructions, mais d'avoir 6 ports d'émission. Le processeur charge et décode 4 instructions, les quatre instructions sont envoyées sur les ports d'émission adéquat. Le cout en transistors est alors situé entre celui d'un CPU quadruple émission et sextuple émission. Et les performances sont aussi entre les deux. Pour les performances, c'est parce que cela permet d'avoir un CPU quadruple émission avec bien moins de contraintes d’appariement. Imaginez qu'on se soit limité à quatre ports d'émission : il aurait fallu mettre plusieurs unités sur le même port d'émission. En conséquence, il y aurait eu des combinaisons interdites, comme émettre deux instructions entières + un branchement + un accès mémoire ; ou deux instructions entières + une opération flottante + un accès mémoire. De tells appariement interdits sont possibles en utilisant les six ports d'émission. ==L'émission multiple des accès mémoire== Les premiers processeurs superscalaires ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles. Par contre, c'est autre chose sur les processeurs superscalaires larges. Vu qu'ils travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares. L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. La ''load/store queue'' reste une structure unique, mais on peut lui rajouter des ports. Et à ce peit jeu, il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire. La première duplique les unités de calcul, mais pas les ports d'accès au cache. Les accès mémoire sont alors accumulés dans la ''load/store queue'', qui sont ensuite traités un par un par le cache. Un processeur utilisant cette technique est, par exemple, capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. Un exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. Ces processeurs pouvaient calculer trois adresses simultanément, mais ne pouvaient faire qu'un seul accès au cache par cycle. Le cout en circuit est modéré : les unités de calcul coutent très peu, les ports de la ''load/store queue'' coutent déjà un peu plus. La seconde méthode ajoute des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultanées. Il faut alors dupliquer des unités de calcul, rajouter les ports sur le cache, mais aussi rajouter des ports sur la ''load/store queue'', pour qu'elle échange des données/commandes mémoire avec le cache. Le cout en circuits est alors conséquent Quelques processeurs utilisaient des solutions intermédiaires, un exemple classique étant les Pentium 1 et 2 d'Intel. Mais nous en parlerons dans le chapitre suivant. [[File:Processeur non-superscalaire basique 02.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]] Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre. Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Le Pentium 4 était dans ce cas, mais il était un peu seul dans sa catégorie. Un cas similaire est celui où l'unité mémoire est alimentée par une fenêtre de micro-opération mélangeant instructions mémoire et entières. Les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. Dans ce cas, l'émission multiple est rarement implémentée. Le cas suivant a une file pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique. Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui pouvaient seulement émettre une lecture en même temps qu'une écriture. Ils avaient deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse. [[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]] Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture demande d'utiliser une file d'écriture multiport, ce qui est assez compliqué. Implémenter des lectures en plus est plus simple : il suffit de rajouter une seconde file de µops LOAD et une unité de calcul, alimentées via un second port d'émission. Il est aussi possible de rajouter un second port de lecture au cache, mais ce n'est pas obligatoire. Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse. De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports de lecture et d'écriture car il y a plus de lecture que d'écritures. Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. : Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse. [[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]] Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettent plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail. Pour résumer, l'évolution des CPU superscalaires a été la suivante : * pipeline mémorie fusionné avec le pipeline entier ; * séparation du pipeline mémoire du reste, avec émission parallèle des accès mémoire ; * émission multiple, mais sans dupliquer les ports du cache ; * émission multiple, avec émission simultanée d'une lecture et d’une écriture dans le cache ; * émission multiple avec plusieurs accès en lecture au cache. ==Les optimisations des processeurs superscalaires larges== Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul. ===L'étape de chargement d'un CPU superscalaire large=== Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 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 autoaligné.]] Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements ! [[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]] Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez 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 microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total). Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois 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 microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement. Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations. ===La micro-fusion des processeurs Intel et AMD=== La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''. Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''. Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout. La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Le parallélisme mémoire | prevText=Le parallélisme mémoire | next=Exemples de microarchitectures CPU : le cas du x86 | nextText=Exemples de microarchitectures CPU : le cas du x86 }} </noinclude> 4ngq89swkbfd02e95l9483plrh64y7u 772681 772680 2026-09-21T14:35:38Z Mewtow 31375 /* La double émission entière-flottante */ 772681 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 tout premiers processeurs superscalaires pouvaient émettre deux µops en même temps, ce qui portait le nom de '''double émission'''. Mais le nombre a rapidement augmenté et les processeurs modernes 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. ==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. Par exemple, l'unité de chargement charge deux fois plus d'instructions, mais n'est pas modifiée en profondeur. L'unité d'émission et les circuits d'exécution dans le désordre sont eux aussi altérés. Les unités de calcul sont souvent dupliquées, sauf sur les tout premiers CPU superscalaires. {|class="wikitable" |- ! rowspan="2" | Processeur sans émission multiple | rowspan="2" | Chargement | rowspan="2" | Décodage | rowspan="2" | Renommage | rowspan="2" | Émission | Exécution / ALU | rowspan="2" | ''Commit''/ROB |- | Exécution / ALU |- ! rowspan="4" | Processeur superscalaire | rowspan="4" | Chargement | rowspan="2" | Décodage | rowspan="4" | Renommage | rowspan="4" | Émission | Exécution / ALU | rowspan="4" | ''Commit''/ROB |- | Exécution / ALU |- | rowspan="2" | Décodage | Exécution / ALU |- | Exécution / ALU |} Voyons ce qu'il en est dans le détail. ===Les unités de calcul d'un processeur superscalaire=== Émettre N instructions simultanément demande implicitement d'exécuter N instructions par cycle, voire plus si des instructions multicycles viennent mettre leur grain de sel. Intuitivement, on se dit que les unités de calcul doivent être dupliquées. Et c'est une solution viable pour les processeurs très simples, avec seulement quelques ALU entières. Par exemple, un processeur RISC basique avec seulement une ALU entière et un ''barrel shifter'' peut être rendu superscalaire en dupliquant les deux circuits. Le problème, est que cette solution marche assez mal dès que le processeur contient des circuits multiplieurs et/ou une FPU. Par exemple, un processeur à double émission devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, etc. 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 ! Par contre, l'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission. Un autre cas extrême est celui où aucune unité de calcul n'est dupliquée. Pour comprendre comment c'est possible rappelez-vous qu'un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et éventuellement une unité de branchement. Cela suffit pour créer un processeur quadruple émission, très limité. Un tel processeur peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire. Le problème est qu'il est rare que l'on ait à exécuter en même temps un calcul entier, un calcul flottant, un accès mémoire et un branchement. La quadruple émission n'est donc que rarement utilisée. En pratique, les seules combinaisons fréquentes sont les combinaisons calcul + accès mémoire et calcul + branchement. Les combinaisons calcul entier + calcul flottant sont aussi possibles, mais plus rares. Mais on dépasse rarement la double émission. En clair, à partir de la quadruple émission, on ne peut plus se contenter d'une ALU, d'une FPU, d'une unité de branchement et d'une unité mémoire. Les processeurs superscalaires larges n'ont pas le choix que de rajouter des unités de calcul. Reste à voir ce qui est dupliqué en pratique. 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. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant. La conséquence est que les processeurs superscalaires ont des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a des paires d'instructions autorisées et des paires interdites. Par exemple, l'exécution simultanée de deux branchements est interdite, les branchements sont exécutés l'un après l'autre. Mais il est possible d'émettre un branchement en même temps qu'une autre instruction, en espérant que la prédiction de branchement ait fait une bonne prédiction. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur. En pratique, ces contraintes d’appariement ont peu d’impact. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière, le ratio est le même quand on remplace la multiplication par un branchement ou un accès mémoire. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Deux branchements consécutifs sont quant à eux plus fréquents, idem pour deux accès mémoire. Dupliquer des ALU entières est donc la priorité, suivi par la duplication des unités de branchement, puis celle des accès mémoire, pour terminer par le multiplieur et la FPU. ===Le chargement de plusieurs instructions consécutives=== Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large. Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence. Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas. Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement non-pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc. [[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]] ===Le décodage parallèle des instructions=== Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits. Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur. Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! ===L'unité d'émission d'un processeur superscalaire=== La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. Nous verrons pourquoi dans la suite, mais on peut dire que c'est juste plus simple et plus efficace ainsi. Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop candidate, sous entendu "candidate à l'émission". L'unité d'émission détecte les dépendances entre la µop candidate et les µops en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en cours d'exécution dans le pipeline. mais en plus, elle doit tester les dépendances entre les µops candidates. Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément, il doit détecter les paires d'instructions dépendantes et les sérialiser. Avec une fenêtre d'instruction, il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil gérent les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires modernes utilisent tous une fenêtre d'instruction centralisée ou décentralisée, et non des ''scoreboard''. Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps, là encore en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire peut émettre 8 µops à la fois, on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle. ===L'unité de renommage superscalaire=== Avec la superscalarité, l'unité de renommage de registre doit gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance de registre, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées. Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat. [[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]] Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante. [[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]] Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande. [[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]] ===Le banc de registre d'un processeur superscalaire=== Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement. 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. ==L'évolution historique des processeurs superscalaires== Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs triple émission permettent eux d’émettre trois instructions à la fois. Les tout premiers processeurs superscalaires avaient un budget en transistors très limité, ce qui fait qu'ils ne pouvaient pas être "totalement superscalaires". Par exemple, un processeur double émission ne pouvait pas émettre deux opérations entières en même temps, ni deux opérations flottantes, ni une opération entière avec un accès mémoire. Il y avait des paires d'instructions autorisées et des paires interdites. Les '''contraintes d'appariement''' sur les instructions à émettre en même temps étaient très strictes. L'intérêt de ces contraintes d’appariement est que le processeur était très simple, il n'y avait pas besoin de rajouter des circuits complexes pour rendre le processeur superscalaire. Les CPU superscalaires historiques ne faisaient que mieux utiliser des unités de calcul déjà présentes. Prenons un processeur non-superscalaire simple, illustré ci-dessous. Il contient : une unité de chargement, un décodeur, une unité d'émission, une ALU, une FPU, les bancs de registre entier et flottants, et une unité mémoire. Nous avons placé l'unité mémoire à la suite de l'ALU entière, car ce sera important pour les explications qui vont suivre. [[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]] Les processeurs superscalaires historiques modifiaient assez peu le schéma précédent. L'unité de chargement était adaptée pour lire plusieurs instructions depuis la mémoire, il y avait un seul décodeur, l'unité d'émission était assez peu modifiée. Mais une chose était certaine : aucune unité de calcul n'était dupliquée, le banc de registre n'était presque pas modifié. A la rigueur, gérer la triple 'émission avec les branchements demandait parfois d'ajouter des ports de lecture sur le banc de registre généraux, pas plus. ===La double émission entière-flottante=== Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est d'émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Le processeur a donc deux pipelines séparés : un pour les micro-opérations entières, un autre pour les micro-opérations flottantes. On parle alors de '''double émission entière-flottante'''. {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 |- | ALU entière || FPU |} La mise en œuvre utilise assez peu de circuits, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité. La plupart des premiers processeurs superscalaires étaient de ce type. Les exemples les plus notables sont les processeurs POWER 1 et ses dérivés comme le ''RISC Single Chip''. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle. [[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]] La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisations visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate]. ===L'émission parallèle des branchements=== Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur. 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 |} 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 modifier le banc de registres. Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. [[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]] Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications et les divisions ? Pour étudier ces questions, nous allons prendre le cas d'un processeur capable d’émettre trois opérations en même temps : deux opérations entières, et une flottante, potentiellement d'autres. Le cas des opérations complexes est assez intéressant. Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les ALUs en N exemplaires. Mais ce n'est jamais fait en pratique. Concrètement, seules les ALU entières simples le sont, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ne sont pas dupliqués en N exemplaires, le cout en transistors serait trop important. Pour un processeur à double émission, l'unité d'émission a deux ports d'émission, pour émettre deux µops entières. Les deux ports ont chacun leur ALU entière, mais l'un d'entre eux a est aussi relié au circuit multiplier. En clair, cela permet d'émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Idem pour les opérations simples comme les soustractions, les opérations bit à bit, etc. On peut en exécuter deux, ou en coupler une avec une multiplication. Faire ainsi n'a pas de cout en performance significatif, comparé à une duplication totale des ALUs entières. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur serait inutile. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle. [[File:Emission multiple des opérations entières, implémentation naive.png|centre|vignette|upright=2|Émission multiple des opérations entières, implémentation naïve.]] Il en est de même avec les décalages, vu que le ''barrel shifter'' n'est pas dupliqué. Il est donc possible d'émettre deux opérations de base, un décalage avec une opération de base, mais pas deux décalages en parallèle. En général, le ''barrel shifter'' et le multiplieur sont placés sur deux ports d'émission séparés. Cela permet ainsi de faire un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas. [[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Emission multiple des opérations entières, implémentation courante]] Pour les branchements, deux solutions sont possibles. Avec la première, il y a émission parallèle des branchements, à savoir qu'il est possible d'émettre en branchement en parallèle de deux-trois instructions entières. Et cela impose la présence d'une unité de branchement séparée, avec son port d'émission. Avec la seconde, les branchements sont traités comme des instructions entières. Il peut y avoir une unité de branchement séparée, mais ce n'est pas souvent le cas. En général, les branchements sont exécutés dans une ALU entière. [[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Emission série et parallèle des branchements]] Les accès mémoire sont traités comme des opérations entières un peu spéciales, sur les processeurs superscalaires historiques. En clair, le processeur peut émettre deux opérations entières, ou une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent. Les raisons à cela étaient nombreuses. Et ces raisons sont encore valables sur des petits processeurs destinés à l'embarqué ou l'informatique industrielle. Seuls les processeurs haute performance modernes font différemment. La raison principale est le cout en circuit. Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse. Pas besoin de rajouter une unité de calcul d'adresse séparée. Mais surtout, permettre l'émission parallèle des accès mémoire ferait passer un processeur triple émission à de la quadruple émission, avec tous les couts que ça implique. Il faudrait rajouter des ports de lecture/écriture au banc de registre généraux, pour traiter un accès mémoire en plus du reste. Les circuits d'émission et de chargement devraient être élargit, aussi. Et quitte à payer un tel cout en circuit, il serait plus rentable de l'utiliser pour émettre une quatrième opération entière qu'un accès mémoire. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand-chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2. ===Les CPU superscalaires à plusieurs unités spécialisées=== Le classique deux opérations entières + une flottante était l'idéal pour la triple émission. Même les processeurs modernes à triple émission restent sur cette "norme", car elle est proche de l'optimal. Mais arrivé à la quadruple émission, les possibilités sont devenues plus diverses. L'évolution des CPU superscalaires ont évolués dans des directions différentes, chacun faisant à sa sauce, les choses sont devenues plus confuses. Le seul point commun est qu'ils ont eu tendance à rajouter des unités de calcul. Un point important est que la quadruple émission permet de nombreuses possibilités, mais la plupart ne sont pas intéressantes. Pour donner un exemple concret, prenons un CPU superscalaire très simple, comprenant une ALU entière, une FPU, une unité de branchement, et une unité mémoire. Il est possible de créer un CPU quadruple émission avec ces unités, mais le gain en performance n'en vaudrait pas la peine pour le cout en circuit. Il est rare qu'un bloc de 4 instructions contienne exactement une opération entière, une flottante, un branchement et un accès mémoire. En pratique, les seules combinaisons fréquentes sont les combinaisons calcul + accès mémoire et calcul + branchement. Les combinaisons calcul entier + calcul flottant sont aussi possibles, mais plus rares. Mais on dépasse rarement la double émission. Il est préférable d'utiliser ces unités dans un CPU double ou triple émission, en retirant l'unité de branchement et/ou en couplant l'unité mémoire à l'ALU. En clair, une fois arrivé à la quadruple émission, on ne peut plus se contenter d'une ALU, d'une FPU, d'une unité de branchement et d'une unité mémoire. Les processeurs superscalaires larges n'ont pas le choix que de rajouter des unités de calcul, voire des unités mémoire. Et en pratique, ils ont privilégié l'ajout d'ALU entières, avant de dupliquer les autres circuits. La raison est que les programmes informatiques utilisent beaucoup plus d'additions/soustractions/comparaisons que le reste. Il y a environ 5 opérations entières de base pour une multiplication entière, le ratio est le même quand on remplace la multiplication par un branchement ou un accès mémoire. En conséquence, les ALU entières ont été dupliquées en premier, et dans de nombreux exemplaires. Reste à voir comment a été dupliqué le reste. Les multiplieurs n'ont pas été dupliqués, sauf sur les processeurs superscalaires très larges. La raison est qu'il est rare de devoir exécuter deux multiplications à la fois. C'est la même chose pour le ''barrel shifter''. Deux branchements consécutifs sont quant à eux plus fréquents, c'est même assez courant. Aussi, les unités de branchement ont été dupliquées en second lieu, après les ALU entières. Avoir plusieurs accès mémoire proches est assez courant, surtout sur les CPU de type CISC. Aussi, les unités mémoire ont été dupliquées en troisième lieu. Pour résumer, dupliquer des ALU entières est donc la priorité, suivi par la duplication des unités de branchement, puis celle des accès mémoire, pour terminer par le multiplieur. La FPU est un peu à part. Une autre tendance a été de séparer l'unité mémoire des ALU entières, ce qui a permis l''''émission parallèle des µops mémoire'''. Comme son nom l'indique, l'idée est de séparer les µops mémoire des opérations entières. Il devient alors possible d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière. La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple. La '''double émission flottante''' a été utilisée sur les processeurs Alpha. L'idée est d'émettre deux opérations flottantes à la fois. Sauf qu'une implémentation naïve demanderait de dupliquer la FPU, ce qui aurait un cout en circuit rédhibitoire. Pour rappel, les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant. L'idée est d'avoir des ports d'émission séparés pour l'additionneur flottant et le multiplieur flottant. Le processeur peut alors émettre une addition flottante en même temps qu'une multiplication flottante. Les autres circuits de calcul flottant sont répartis sur ces deux ports d'émission flottants. L'avantage est que cela se marie bien avec l'usage d'une fenêtre d'instruction séparée pour les opérations flottantes. La fenêtre d'instruction a alors deux ports séparés, au lieu d'un seul. Rajouter un second port d'émission flottant n'est pas trop un problème, car le cout lié à l'ajout d'un port n'est pas linéaire. Passer de un port à deux a un cout tolérable, bien plus que de passer de 3 ports à 4 ou de 4 à 5. Il est possible de dupliquer l'additionneur et le multiplieur flottant, ce qui permet d'émettre deux additions et multiplications flottantes en même temps. C'est ce qui est fait sur les processeurs AMD d'architecture Zen 1 et 2. Ils ont deux additionneurs flottants par cœur, deux multiplieurs flottants, chacun avec leur propre port d'émission. Les performances en calcul flottant sont assez impressionnantes pour un processeur de l'époque. [[File:ZEN - émission multiple flottante.png|centre|vignette|upright=2|Microarchitecture Zen 1 d'AMD.]] ==Les ports d'émission et de décodage== Les ports d'émission et de décodage sont des ressources matérielles comme les autres. Dans cette section, nous allons parler de concepts liés aux ports d'émission et de décodage. Nous allons notamment parler du partage des ports d'émission, ainsi que des relations numériques entre ces deux types de ports. ===Le partage des ports d'émission et la ''port contention''=== Il est possible de partager un port d'émission, à savoir de connecter un port d'émission à plusieurs unités de calcul. Nous en avons vu un exemple plus haut, avec l'émission multiple des opérations entières. On peut par exemple connecter un multiplieur et une ALU entière sur le même port d'émission. Pour le moment, nous n'avons vu que des cas où un port d'émission est relié soit à des ALU entières, soit à des FPU, pas aux deux. Mais il existe des processeurs où un même port d'émission alimente à la fois une ALU entière et une FPU. Par exemple, on peut relier un additionneur flottant sur le même port qu'une ALU entière. Il faut noter que cela implique une fenêtre d'instruction centralisée, capable de mettre en attente micro-opérations entières et flottantes. Un exemple est celui des processeurs Core 2 Duo. Ils disposent de 6 ports d'émission, dont 3 ports dédiés à l'unité mémoire. Les 3 ports restants alimentent chacun : une ALU entière, un circuit de calcul flottant et une unité de calcul SSE (une unité de calcul SIMD, qu'on abordera dans quelques chapitres). * Le premier port alimente une ALU entière simple et un multiplieur/diviseur flottant. * Le second alimente une ALU entière, un multiplieur entier et un additionneur flottant. * Le troisième alimente une ALU entière, sans circuit flottant dédié. Dans le même genre, il est possible de partager un port d'émission entre l'unité mémoire et une ALU entière. Cela permet d'utiliser l'ALU entière pour les calculs d'adresse, ce qui évite d'avoir à utiliser une unité de calcul d'adresse distincte. Un exemple est celui du processeur superscalaire double émission Power PC 440. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. L'organisation en question permet soit d'émettre un accès mémoire en même temps qu'une opération entière, soit d'émettre deux opérations entières simples, soit d’émettre une multiplication et une addition/soustraction/comparaison. Une organisation simple, mais très efficace ! [[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]] Regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles, qui ne se présentent que pour certaines combinaisons de micro-opérations bien précises. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port. Le nombre de ports peut être un facteur limitant pour la performance dans certaines situations de '''''port contention''''' où on a assez d'ALU pour exécuter N micro-opérations, mais où la répartition des ALUs sur les ports l’empêche. Mais on peut réduire la ''port contention'' en répartissant correctement les ALU sur les ports d'émission. Le choix en question dépendent fortement du fait qu'en moyenne, que certaines paires d'instructions sont plus fréquentes que d'autres. Tout cela guide ce choix de répartition des ALUs sur les ports. Nous venons de voir que la répartition des ALU sur les ports d'émission est très variable d'un processeur à l'autre. Les divers choix possibles sont tous des compromis entre deux forces : réduire le nombre de ports d'émission d'un côté, garder de bonnes performances en réduisant la ''port contention'' de l'autre. Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction relativement simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ? Par contre, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal réparti les ALU. ===La séparation entre largeur de décodage et d'émission=== Sur les processeurs superscalaires larges, le processeur peut émettre plus d'instructions qu'il peut en décoder. Plus haut, j'ai dit que l'unité d'émission avait parfois plus de ports d'émission que de ports de décodage. La raison est assez simple, et elle s'explique assez bien avec un exemple. Prenons un processeur avec trois ALU entières, une FPU, une unité de branchement et une unité mémoire. Chacune a son propre port d'émission, ce qui fait que le CPU est capable d'émettre 6 µops. Du moins, c'est la théorie, car les conditions pour sont assez rares. Il faut qu'en chargeant un paquet de 6 instructions, il y ait exactement trois instructions entières, une flottante, un branchement et un accès mémoire. Si le paquet contient 5 instructions entières et un branchement, seules quatre instructions sont émises. Dans cet exemple, dans la grande majorité des cas, seuls 4 ports seront utilisés en moyenne et deux seront sous-utilisés. Il s'agit là d'une moyenne, qui dépend du code, et des instructions exactes à exécuter. Cependant, cette moyenne nous donne un moyen d'économiser beaucoup de circuits pour un cout en performance mineur. L'idée est de ne charger que des blocs de 4 instructions, mais d'avoir 6 ports d'émission. Le processeur charge et décode 4 instructions, les quatre instructions sont envoyées sur les ports d'émission adéquat. Le cout en transistors est alors situé entre celui d'un CPU quadruple émission et sextuple émission. Et les performances sont aussi entre les deux. Pour les performances, c'est parce que cela permet d'avoir un CPU quadruple émission avec bien moins de contraintes d’appariement. Imaginez qu'on se soit limité à quatre ports d'émission : il aurait fallu mettre plusieurs unités sur le même port d'émission. En conséquence, il y aurait eu des combinaisons interdites, comme émettre deux instructions entières + un branchement + un accès mémoire ; ou deux instructions entières + une opération flottante + un accès mémoire. De tells appariement interdits sont possibles en utilisant les six ports d'émission. ==L'émission multiple des accès mémoire== Les premiers processeurs superscalaires ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles. Par contre, c'est autre chose sur les processeurs superscalaires larges. Vu qu'ils travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares. L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. La ''load/store queue'' reste une structure unique, mais on peut lui rajouter des ports. Et à ce peit jeu, il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire. La première duplique les unités de calcul, mais pas les ports d'accès au cache. Les accès mémoire sont alors accumulés dans la ''load/store queue'', qui sont ensuite traités un par un par le cache. Un processeur utilisant cette technique est, par exemple, capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. Un exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. Ces processeurs pouvaient calculer trois adresses simultanément, mais ne pouvaient faire qu'un seul accès au cache par cycle. Le cout en circuit est modéré : les unités de calcul coutent très peu, les ports de la ''load/store queue'' coutent déjà un peu plus. La seconde méthode ajoute des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultanées. Il faut alors dupliquer des unités de calcul, rajouter les ports sur le cache, mais aussi rajouter des ports sur la ''load/store queue'', pour qu'elle échange des données/commandes mémoire avec le cache. Le cout en circuits est alors conséquent Quelques processeurs utilisaient des solutions intermédiaires, un exemple classique étant les Pentium 1 et 2 d'Intel. Mais nous en parlerons dans le chapitre suivant. [[File:Processeur non-superscalaire basique 02.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]] Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre. Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Le Pentium 4 était dans ce cas, mais il était un peu seul dans sa catégorie. Un cas similaire est celui où l'unité mémoire est alimentée par une fenêtre de micro-opération mélangeant instructions mémoire et entières. Les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. Dans ce cas, l'émission multiple est rarement implémentée. Le cas suivant a une file pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique. Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui pouvaient seulement émettre une lecture en même temps qu'une écriture. Ils avaient deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse. [[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]] Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture demande d'utiliser une file d'écriture multiport, ce qui est assez compliqué. Implémenter des lectures en plus est plus simple : il suffit de rajouter une seconde file de µops LOAD et une unité de calcul, alimentées via un second port d'émission. Il est aussi possible de rajouter un second port de lecture au cache, mais ce n'est pas obligatoire. Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse. De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports de lecture et d'écriture car il y a plus de lecture que d'écritures. Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. : Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse. [[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]] Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettent plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail. Pour résumer, l'évolution des CPU superscalaires a été la suivante : * pipeline mémorie fusionné avec le pipeline entier ; * séparation du pipeline mémoire du reste, avec émission parallèle des accès mémoire ; * émission multiple, mais sans dupliquer les ports du cache ; * émission multiple, avec émission simultanée d'une lecture et d’une écriture dans le cache ; * émission multiple avec plusieurs accès en lecture au cache. ==Les optimisations des processeurs superscalaires larges== Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul. ===L'étape de chargement d'un CPU superscalaire large=== Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 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 autoaligné.]] Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements ! [[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]] Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez 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 microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total). Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois 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 microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement. Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations. ===La micro-fusion des processeurs Intel et AMD=== La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''. Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''. Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout. La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Le parallélisme mémoire | prevText=Le parallélisme mémoire | next=Exemples de microarchitectures CPU : le cas du x86 | nextText=Exemples de microarchitectures CPU : le cas du x86 }} </noinclude> qe36962nrezf03sdsq5aojsd57ldeub 772682 772681 2026-09-21T14:35:59Z Mewtow 31375 /* La double émission entière-flottante */ 772682 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 tout premiers processeurs superscalaires pouvaient émettre deux µops en même temps, ce qui portait le nom de '''double émission'''. Mais le nombre a rapidement augmenté et les processeurs modernes 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. ==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. Par exemple, l'unité de chargement charge deux fois plus d'instructions, mais n'est pas modifiée en profondeur. L'unité d'émission et les circuits d'exécution dans le désordre sont eux aussi altérés. Les unités de calcul sont souvent dupliquées, sauf sur les tout premiers CPU superscalaires. {|class="wikitable" |- ! rowspan="2" | Processeur sans émission multiple | rowspan="2" | Chargement | rowspan="2" | Décodage | rowspan="2" | Renommage | rowspan="2" | Émission | Exécution / ALU | rowspan="2" | ''Commit''/ROB |- | Exécution / ALU |- ! rowspan="4" | Processeur superscalaire | rowspan="4" | Chargement | rowspan="2" | Décodage | rowspan="4" | Renommage | rowspan="4" | Émission | Exécution / ALU | rowspan="4" | ''Commit''/ROB |- | Exécution / ALU |- | rowspan="2" | Décodage | Exécution / ALU |- | Exécution / ALU |} Voyons ce qu'il en est dans le détail. ===Les unités de calcul d'un processeur superscalaire=== Émettre N instructions simultanément demande implicitement d'exécuter N instructions par cycle, voire plus si des instructions multicycles viennent mettre leur grain de sel. Intuitivement, on se dit que les unités de calcul doivent être dupliquées. Et c'est une solution viable pour les processeurs très simples, avec seulement quelques ALU entières. Par exemple, un processeur RISC basique avec seulement une ALU entière et un ''barrel shifter'' peut être rendu superscalaire en dupliquant les deux circuits. Le problème, est que cette solution marche assez mal dès que le processeur contient des circuits multiplieurs et/ou une FPU. Par exemple, un processeur à double émission devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, etc. 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 ! Par contre, l'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission. Un autre cas extrême est celui où aucune unité de calcul n'est dupliquée. Pour comprendre comment c'est possible rappelez-vous qu'un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et éventuellement une unité de branchement. Cela suffit pour créer un processeur quadruple émission, très limité. Un tel processeur peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire. Le problème est qu'il est rare que l'on ait à exécuter en même temps un calcul entier, un calcul flottant, un accès mémoire et un branchement. La quadruple émission n'est donc que rarement utilisée. En pratique, les seules combinaisons fréquentes sont les combinaisons calcul + accès mémoire et calcul + branchement. Les combinaisons calcul entier + calcul flottant sont aussi possibles, mais plus rares. Mais on dépasse rarement la double émission. En clair, à partir de la quadruple émission, on ne peut plus se contenter d'une ALU, d'une FPU, d'une unité de branchement et d'une unité mémoire. Les processeurs superscalaires larges n'ont pas le choix que de rajouter des unités de calcul. Reste à voir ce qui est dupliqué en pratique. 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. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant. La conséquence est que les processeurs superscalaires ont des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a des paires d'instructions autorisées et des paires interdites. Par exemple, l'exécution simultanée de deux branchements est interdite, les branchements sont exécutés l'un après l'autre. Mais il est possible d'émettre un branchement en même temps qu'une autre instruction, en espérant que la prédiction de branchement ait fait une bonne prédiction. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur. En pratique, ces contraintes d’appariement ont peu d’impact. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière, le ratio est le même quand on remplace la multiplication par un branchement ou un accès mémoire. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Deux branchements consécutifs sont quant à eux plus fréquents, idem pour deux accès mémoire. Dupliquer des ALU entières est donc la priorité, suivi par la duplication des unités de branchement, puis celle des accès mémoire, pour terminer par le multiplieur et la FPU. ===Le chargement de plusieurs instructions consécutives=== Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large. Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence. Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas. Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement non-pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc. [[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]] ===Le décodage parallèle des instructions=== Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits. Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur. Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! ===L'unité d'émission d'un processeur superscalaire=== La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. Nous verrons pourquoi dans la suite, mais on peut dire que c'est juste plus simple et plus efficace ainsi. Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop candidate, sous entendu "candidate à l'émission". L'unité d'émission détecte les dépendances entre la µop candidate et les µops en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en cours d'exécution dans le pipeline. mais en plus, elle doit tester les dépendances entre les µops candidates. Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément, il doit détecter les paires d'instructions dépendantes et les sérialiser. Avec une fenêtre d'instruction, il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil gérent les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires modernes utilisent tous une fenêtre d'instruction centralisée ou décentralisée, et non des ''scoreboard''. Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps, là encore en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire peut émettre 8 µops à la fois, on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle. ===L'unité de renommage superscalaire=== Avec la superscalarité, l'unité de renommage de registre doit gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance de registre, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées. Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat. [[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]] Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante. [[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]] Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande. [[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]] ===Le banc de registre d'un processeur superscalaire=== Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement. 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. ==L'évolution historique des processeurs superscalaires== Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs triple émission permettent eux d’émettre trois instructions à la fois. Les tout premiers processeurs superscalaires avaient un budget en transistors très limité, ce qui fait qu'ils ne pouvaient pas être "totalement superscalaires". Par exemple, un processeur double émission ne pouvait pas émettre deux opérations entières en même temps, ni deux opérations flottantes, ni une opération entière avec un accès mémoire. Il y avait des paires d'instructions autorisées et des paires interdites. Les '''contraintes d'appariement''' sur les instructions à émettre en même temps étaient très strictes. L'intérêt de ces contraintes d’appariement est que le processeur était très simple, il n'y avait pas besoin de rajouter des circuits complexes pour rendre le processeur superscalaire. Les CPU superscalaires historiques ne faisaient que mieux utiliser des unités de calcul déjà présentes. Prenons un processeur non-superscalaire simple, illustré ci-dessous. Il contient : une unité de chargement, un décodeur, une unité d'émission, une ALU, une FPU, les bancs de registre entier et flottants, et une unité mémoire. Nous avons placé l'unité mémoire à la suite de l'ALU entière, car ce sera important pour les explications qui vont suivre. [[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]] Les processeurs superscalaires historiques modifiaient assez peu le schéma précédent. L'unité de chargement était adaptée pour lire plusieurs instructions depuis la mémoire, il y avait un seul décodeur, l'unité d'émission était assez peu modifiée. Mais une chose était certaine : aucune unité de calcul n'était dupliquée, le banc de registre n'était presque pas modifié. A la rigueur, gérer la triple 'émission avec les branchements demandait parfois d'ajouter des ports de lecture sur le banc de registre généraux, pas plus. ===La double émission entière-flottante=== Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est d'émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Le processeur a donc deux pipelines séparés : un pour les micro-opérations entières, un autre pour les micro-opérations flottantes. On parle alors de '''double émission entière-flottante'''. {|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, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité. La plupart des premiers processeurs superscalaires étaient de ce type. Les exemples les plus notables sont les processeurs POWER 1 et ses dérivés comme le ''RISC Single Chip''. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle. [[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]] La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisations visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate]. ===L'émission parallèle des branchements=== Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur. 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 |} 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 modifier le banc de registres. Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. [[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]] Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications et les divisions ? Pour étudier ces questions, nous allons prendre le cas d'un processeur capable d’émettre trois opérations en même temps : deux opérations entières, et une flottante, potentiellement d'autres. Le cas des opérations complexes est assez intéressant. Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les ALUs en N exemplaires. Mais ce n'est jamais fait en pratique. Concrètement, seules les ALU entières simples le sont, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ne sont pas dupliqués en N exemplaires, le cout en transistors serait trop important. Pour un processeur à double émission, l'unité d'émission a deux ports d'émission, pour émettre deux µops entières. Les deux ports ont chacun leur ALU entière, mais l'un d'entre eux a est aussi relié au circuit multiplier. En clair, cela permet d'émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Idem pour les opérations simples comme les soustractions, les opérations bit à bit, etc. On peut en exécuter deux, ou en coupler une avec une multiplication. Faire ainsi n'a pas de cout en performance significatif, comparé à une duplication totale des ALUs entières. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur serait inutile. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle. [[File:Emission multiple des opérations entières, implémentation naive.png|centre|vignette|upright=2|Émission multiple des opérations entières, implémentation naïve.]] Il en est de même avec les décalages, vu que le ''barrel shifter'' n'est pas dupliqué. Il est donc possible d'émettre deux opérations de base, un décalage avec une opération de base, mais pas deux décalages en parallèle. En général, le ''barrel shifter'' et le multiplieur sont placés sur deux ports d'émission séparés. Cela permet ainsi de faire un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas. [[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Emission multiple des opérations entières, implémentation courante]] Pour les branchements, deux solutions sont possibles. Avec la première, il y a émission parallèle des branchements, à savoir qu'il est possible d'émettre en branchement en parallèle de deux-trois instructions entières. Et cela impose la présence d'une unité de branchement séparée, avec son port d'émission. Avec la seconde, les branchements sont traités comme des instructions entières. Il peut y avoir une unité de branchement séparée, mais ce n'est pas souvent le cas. En général, les branchements sont exécutés dans une ALU entière. [[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Emission série et parallèle des branchements]] Les accès mémoire sont traités comme des opérations entières un peu spéciales, sur les processeurs superscalaires historiques. En clair, le processeur peut émettre deux opérations entières, ou une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent. Les raisons à cela étaient nombreuses. Et ces raisons sont encore valables sur des petits processeurs destinés à l'embarqué ou l'informatique industrielle. Seuls les processeurs haute performance modernes font différemment. La raison principale est le cout en circuit. Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse. Pas besoin de rajouter une unité de calcul d'adresse séparée. Mais surtout, permettre l'émission parallèle des accès mémoire ferait passer un processeur triple émission à de la quadruple émission, avec tous les couts que ça implique. Il faudrait rajouter des ports de lecture/écriture au banc de registre généraux, pour traiter un accès mémoire en plus du reste. Les circuits d'émission et de chargement devraient être élargit, aussi. Et quitte à payer un tel cout en circuit, il serait plus rentable de l'utiliser pour émettre une quatrième opération entière qu'un accès mémoire. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand-chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2. ===Les CPU superscalaires à plusieurs unités spécialisées=== Le classique deux opérations entières + une flottante était l'idéal pour la triple émission. Même les processeurs modernes à triple émission restent sur cette "norme", car elle est proche de l'optimal. Mais arrivé à la quadruple émission, les possibilités sont devenues plus diverses. L'évolution des CPU superscalaires ont évolués dans des directions différentes, chacun faisant à sa sauce, les choses sont devenues plus confuses. Le seul point commun est qu'ils ont eu tendance à rajouter des unités de calcul. Un point important est que la quadruple émission permet de nombreuses possibilités, mais la plupart ne sont pas intéressantes. Pour donner un exemple concret, prenons un CPU superscalaire très simple, comprenant une ALU entière, une FPU, une unité de branchement, et une unité mémoire. Il est possible de créer un CPU quadruple émission avec ces unités, mais le gain en performance n'en vaudrait pas la peine pour le cout en circuit. Il est rare qu'un bloc de 4 instructions contienne exactement une opération entière, une flottante, un branchement et un accès mémoire. En pratique, les seules combinaisons fréquentes sont les combinaisons calcul + accès mémoire et calcul + branchement. Les combinaisons calcul entier + calcul flottant sont aussi possibles, mais plus rares. Mais on dépasse rarement la double émission. Il est préférable d'utiliser ces unités dans un CPU double ou triple émission, en retirant l'unité de branchement et/ou en couplant l'unité mémoire à l'ALU. En clair, une fois arrivé à la quadruple émission, on ne peut plus se contenter d'une ALU, d'une FPU, d'une unité de branchement et d'une unité mémoire. Les processeurs superscalaires larges n'ont pas le choix que de rajouter des unités de calcul, voire des unités mémoire. Et en pratique, ils ont privilégié l'ajout d'ALU entières, avant de dupliquer les autres circuits. La raison est que les programmes informatiques utilisent beaucoup plus d'additions/soustractions/comparaisons que le reste. Il y a environ 5 opérations entières de base pour une multiplication entière, le ratio est le même quand on remplace la multiplication par un branchement ou un accès mémoire. En conséquence, les ALU entières ont été dupliquées en premier, et dans de nombreux exemplaires. Reste à voir comment a été dupliqué le reste. Les multiplieurs n'ont pas été dupliqués, sauf sur les processeurs superscalaires très larges. La raison est qu'il est rare de devoir exécuter deux multiplications à la fois. C'est la même chose pour le ''barrel shifter''. Deux branchements consécutifs sont quant à eux plus fréquents, c'est même assez courant. Aussi, les unités de branchement ont été dupliquées en second lieu, après les ALU entières. Avoir plusieurs accès mémoire proches est assez courant, surtout sur les CPU de type CISC. Aussi, les unités mémoire ont été dupliquées en troisième lieu. Pour résumer, dupliquer des ALU entières est donc la priorité, suivi par la duplication des unités de branchement, puis celle des accès mémoire, pour terminer par le multiplieur. La FPU est un peu à part. Une autre tendance a été de séparer l'unité mémoire des ALU entières, ce qui a permis l''''émission parallèle des µops mémoire'''. Comme son nom l'indique, l'idée est de séparer les µops mémoire des opérations entières. Il devient alors possible d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière. La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple. La '''double émission flottante''' a été utilisée sur les processeurs Alpha. L'idée est d'émettre deux opérations flottantes à la fois. Sauf qu'une implémentation naïve demanderait de dupliquer la FPU, ce qui aurait un cout en circuit rédhibitoire. Pour rappel, les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant. L'idée est d'avoir des ports d'émission séparés pour l'additionneur flottant et le multiplieur flottant. Le processeur peut alors émettre une addition flottante en même temps qu'une multiplication flottante. Les autres circuits de calcul flottant sont répartis sur ces deux ports d'émission flottants. L'avantage est que cela se marie bien avec l'usage d'une fenêtre d'instruction séparée pour les opérations flottantes. La fenêtre d'instruction a alors deux ports séparés, au lieu d'un seul. Rajouter un second port d'émission flottant n'est pas trop un problème, car le cout lié à l'ajout d'un port n'est pas linéaire. Passer de un port à deux a un cout tolérable, bien plus que de passer de 3 ports à 4 ou de 4 à 5. Il est possible de dupliquer l'additionneur et le multiplieur flottant, ce qui permet d'émettre deux additions et multiplications flottantes en même temps. C'est ce qui est fait sur les processeurs AMD d'architecture Zen 1 et 2. Ils ont deux additionneurs flottants par cœur, deux multiplieurs flottants, chacun avec leur propre port d'émission. Les performances en calcul flottant sont assez impressionnantes pour un processeur de l'époque. [[File:ZEN - émission multiple flottante.png|centre|vignette|upright=2|Microarchitecture Zen 1 d'AMD.]] ==Les ports d'émission et de décodage== Les ports d'émission et de décodage sont des ressources matérielles comme les autres. Dans cette section, nous allons parler de concepts liés aux ports d'émission et de décodage. Nous allons notamment parler du partage des ports d'émission, ainsi que des relations numériques entre ces deux types de ports. ===Le partage des ports d'émission et la ''port contention''=== Il est possible de partager un port d'émission, à savoir de connecter un port d'émission à plusieurs unités de calcul. Nous en avons vu un exemple plus haut, avec l'émission multiple des opérations entières. On peut par exemple connecter un multiplieur et une ALU entière sur le même port d'émission. Pour le moment, nous n'avons vu que des cas où un port d'émission est relié soit à des ALU entières, soit à des FPU, pas aux deux. Mais il existe des processeurs où un même port d'émission alimente à la fois une ALU entière et une FPU. Par exemple, on peut relier un additionneur flottant sur le même port qu'une ALU entière. Il faut noter que cela implique une fenêtre d'instruction centralisée, capable de mettre en attente micro-opérations entières et flottantes. Un exemple est celui des processeurs Core 2 Duo. Ils disposent de 6 ports d'émission, dont 3 ports dédiés à l'unité mémoire. Les 3 ports restants alimentent chacun : une ALU entière, un circuit de calcul flottant et une unité de calcul SSE (une unité de calcul SIMD, qu'on abordera dans quelques chapitres). * Le premier port alimente une ALU entière simple et un multiplieur/diviseur flottant. * Le second alimente une ALU entière, un multiplieur entier et un additionneur flottant. * Le troisième alimente une ALU entière, sans circuit flottant dédié. Dans le même genre, il est possible de partager un port d'émission entre l'unité mémoire et une ALU entière. Cela permet d'utiliser l'ALU entière pour les calculs d'adresse, ce qui évite d'avoir à utiliser une unité de calcul d'adresse distincte. Un exemple est celui du processeur superscalaire double émission Power PC 440. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. L'organisation en question permet soit d'émettre un accès mémoire en même temps qu'une opération entière, soit d'émettre deux opérations entières simples, soit d’émettre une multiplication et une addition/soustraction/comparaison. Une organisation simple, mais très efficace ! [[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]] Regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles, qui ne se présentent que pour certaines combinaisons de micro-opérations bien précises. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port. Le nombre de ports peut être un facteur limitant pour la performance dans certaines situations de '''''port contention''''' où on a assez d'ALU pour exécuter N micro-opérations, mais où la répartition des ALUs sur les ports l’empêche. Mais on peut réduire la ''port contention'' en répartissant correctement les ALU sur les ports d'émission. Le choix en question dépendent fortement du fait qu'en moyenne, que certaines paires d'instructions sont plus fréquentes que d'autres. Tout cela guide ce choix de répartition des ALUs sur les ports. Nous venons de voir que la répartition des ALU sur les ports d'émission est très variable d'un processeur à l'autre. Les divers choix possibles sont tous des compromis entre deux forces : réduire le nombre de ports d'émission d'un côté, garder de bonnes performances en réduisant la ''port contention'' de l'autre. Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction relativement simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ? Par contre, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal réparti les ALU. ===La séparation entre largeur de décodage et d'émission=== Sur les processeurs superscalaires larges, le processeur peut émettre plus d'instructions qu'il peut en décoder. Plus haut, j'ai dit que l'unité d'émission avait parfois plus de ports d'émission que de ports de décodage. La raison est assez simple, et elle s'explique assez bien avec un exemple. Prenons un processeur avec trois ALU entières, une FPU, une unité de branchement et une unité mémoire. Chacune a son propre port d'émission, ce qui fait que le CPU est capable d'émettre 6 µops. Du moins, c'est la théorie, car les conditions pour sont assez rares. Il faut qu'en chargeant un paquet de 6 instructions, il y ait exactement trois instructions entières, une flottante, un branchement et un accès mémoire. Si le paquet contient 5 instructions entières et un branchement, seules quatre instructions sont émises. Dans cet exemple, dans la grande majorité des cas, seuls 4 ports seront utilisés en moyenne et deux seront sous-utilisés. Il s'agit là d'une moyenne, qui dépend du code, et des instructions exactes à exécuter. Cependant, cette moyenne nous donne un moyen d'économiser beaucoup de circuits pour un cout en performance mineur. L'idée est de ne charger que des blocs de 4 instructions, mais d'avoir 6 ports d'émission. Le processeur charge et décode 4 instructions, les quatre instructions sont envoyées sur les ports d'émission adéquat. Le cout en transistors est alors situé entre celui d'un CPU quadruple émission et sextuple émission. Et les performances sont aussi entre les deux. Pour les performances, c'est parce que cela permet d'avoir un CPU quadruple émission avec bien moins de contraintes d’appariement. Imaginez qu'on se soit limité à quatre ports d'émission : il aurait fallu mettre plusieurs unités sur le même port d'émission. En conséquence, il y aurait eu des combinaisons interdites, comme émettre deux instructions entières + un branchement + un accès mémoire ; ou deux instructions entières + une opération flottante + un accès mémoire. De tells appariement interdits sont possibles en utilisant les six ports d'émission. ==L'émission multiple des accès mémoire== Les premiers processeurs superscalaires ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles. Par contre, c'est autre chose sur les processeurs superscalaires larges. Vu qu'ils travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares. L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. La ''load/store queue'' reste une structure unique, mais on peut lui rajouter des ports. Et à ce peit jeu, il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire. La première duplique les unités de calcul, mais pas les ports d'accès au cache. Les accès mémoire sont alors accumulés dans la ''load/store queue'', qui sont ensuite traités un par un par le cache. Un processeur utilisant cette technique est, par exemple, capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. Un exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. Ces processeurs pouvaient calculer trois adresses simultanément, mais ne pouvaient faire qu'un seul accès au cache par cycle. Le cout en circuit est modéré : les unités de calcul coutent très peu, les ports de la ''load/store queue'' coutent déjà un peu plus. La seconde méthode ajoute des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultanées. Il faut alors dupliquer des unités de calcul, rajouter les ports sur le cache, mais aussi rajouter des ports sur la ''load/store queue'', pour qu'elle échange des données/commandes mémoire avec le cache. Le cout en circuits est alors conséquent Quelques processeurs utilisaient des solutions intermédiaires, un exemple classique étant les Pentium 1 et 2 d'Intel. Mais nous en parlerons dans le chapitre suivant. [[File:Processeur non-superscalaire basique 02.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]] Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre. Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Le Pentium 4 était dans ce cas, mais il était un peu seul dans sa catégorie. Un cas similaire est celui où l'unité mémoire est alimentée par une fenêtre de micro-opération mélangeant instructions mémoire et entières. Les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. Dans ce cas, l'émission multiple est rarement implémentée. Le cas suivant a une file pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique. Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui pouvaient seulement émettre une lecture en même temps qu'une écriture. Ils avaient deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse. [[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]] Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture demande d'utiliser une file d'écriture multiport, ce qui est assez compliqué. Implémenter des lectures en plus est plus simple : il suffit de rajouter une seconde file de µops LOAD et une unité de calcul, alimentées via un second port d'émission. Il est aussi possible de rajouter un second port de lecture au cache, mais ce n'est pas obligatoire. Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse. De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports de lecture et d'écriture car il y a plus de lecture que d'écritures. Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. : Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse. [[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]] Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettent plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail. Pour résumer, l'évolution des CPU superscalaires a été la suivante : * pipeline mémorie fusionné avec le pipeline entier ; * séparation du pipeline mémoire du reste, avec émission parallèle des accès mémoire ; * émission multiple, mais sans dupliquer les ports du cache ; * émission multiple, avec émission simultanée d'une lecture et d’une écriture dans le cache ; * émission multiple avec plusieurs accès en lecture au cache. ==Les optimisations des processeurs superscalaires larges== Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul. ===L'étape de chargement d'un CPU superscalaire large=== Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 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 autoaligné.]] Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements ! [[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]] Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez 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 microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total). Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois 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 microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement. Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations. ===La micro-fusion des processeurs Intel et AMD=== La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''. Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''. Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout. La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Le parallélisme mémoire | prevText=Le parallélisme mémoire | next=Exemples de microarchitectures CPU : le cas du x86 | nextText=Exemples de microarchitectures CPU : le cas du x86 }} </noinclude> lghfqbqvat0r1ad2urwhxm6i03rlw3i 772683 772682 2026-09-21T14:36:13Z Mewtow 31375 /* L'émission parallèle des branchements */ 772683 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 tout premiers processeurs superscalaires pouvaient émettre deux µops en même temps, ce qui portait le nom de '''double émission'''. Mais le nombre a rapidement augmenté et les processeurs modernes 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. ==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. Par exemple, l'unité de chargement charge deux fois plus d'instructions, mais n'est pas modifiée en profondeur. L'unité d'émission et les circuits d'exécution dans le désordre sont eux aussi altérés. Les unités de calcul sont souvent dupliquées, sauf sur les tout premiers CPU superscalaires. {|class="wikitable" |- ! rowspan="2" | Processeur sans émission multiple | rowspan="2" | Chargement | rowspan="2" | Décodage | rowspan="2" | Renommage | rowspan="2" | Émission | Exécution / ALU | rowspan="2" | ''Commit''/ROB |- | Exécution / ALU |- ! rowspan="4" | Processeur superscalaire | rowspan="4" | Chargement | rowspan="2" | Décodage | rowspan="4" | Renommage | rowspan="4" | Émission | Exécution / ALU | rowspan="4" | ''Commit''/ROB |- | Exécution / ALU |- | rowspan="2" | Décodage | Exécution / ALU |- | Exécution / ALU |} Voyons ce qu'il en est dans le détail. ===Les unités de calcul d'un processeur superscalaire=== Émettre N instructions simultanément demande implicitement d'exécuter N instructions par cycle, voire plus si des instructions multicycles viennent mettre leur grain de sel. Intuitivement, on se dit que les unités de calcul doivent être dupliquées. Et c'est une solution viable pour les processeurs très simples, avec seulement quelques ALU entières. Par exemple, un processeur RISC basique avec seulement une ALU entière et un ''barrel shifter'' peut être rendu superscalaire en dupliquant les deux circuits. Le problème, est que cette solution marche assez mal dès que le processeur contient des circuits multiplieurs et/ou une FPU. Par exemple, un processeur à double émission devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, etc. 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 ! Par contre, l'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission. Un autre cas extrême est celui où aucune unité de calcul n'est dupliquée. Pour comprendre comment c'est possible rappelez-vous qu'un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et éventuellement une unité de branchement. Cela suffit pour créer un processeur quadruple émission, très limité. Un tel processeur peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire. Le problème est qu'il est rare que l'on ait à exécuter en même temps un calcul entier, un calcul flottant, un accès mémoire et un branchement. La quadruple émission n'est donc que rarement utilisée. En pratique, les seules combinaisons fréquentes sont les combinaisons calcul + accès mémoire et calcul + branchement. Les combinaisons calcul entier + calcul flottant sont aussi possibles, mais plus rares. Mais on dépasse rarement la double émission. En clair, à partir de la quadruple émission, on ne peut plus se contenter d'une ALU, d'une FPU, d'une unité de branchement et d'une unité mémoire. Les processeurs superscalaires larges n'ont pas le choix que de rajouter des unités de calcul. Reste à voir ce qui est dupliqué en pratique. 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. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant. La conséquence est que les processeurs superscalaires ont des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a des paires d'instructions autorisées et des paires interdites. Par exemple, l'exécution simultanée de deux branchements est interdite, les branchements sont exécutés l'un après l'autre. Mais il est possible d'émettre un branchement en même temps qu'une autre instruction, en espérant que la prédiction de branchement ait fait une bonne prédiction. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur. En pratique, ces contraintes d’appariement ont peu d’impact. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière, le ratio est le même quand on remplace la multiplication par un branchement ou un accès mémoire. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Deux branchements consécutifs sont quant à eux plus fréquents, idem pour deux accès mémoire. Dupliquer des ALU entières est donc la priorité, suivi par la duplication des unités de branchement, puis celle des accès mémoire, pour terminer par le multiplieur et la FPU. ===Le chargement de plusieurs instructions consécutives=== Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large. Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence. Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas. Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement non-pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc. [[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]] ===Le décodage parallèle des instructions=== Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits. Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur. Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! ===L'unité d'émission d'un processeur superscalaire=== La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. Nous verrons pourquoi dans la suite, mais on peut dire que c'est juste plus simple et plus efficace ainsi. Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop candidate, sous entendu "candidate à l'émission". L'unité d'émission détecte les dépendances entre la µop candidate et les µops en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en cours d'exécution dans le pipeline. mais en plus, elle doit tester les dépendances entre les µops candidates. Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément, il doit détecter les paires d'instructions dépendantes et les sérialiser. Avec une fenêtre d'instruction, il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil gérent les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires modernes utilisent tous une fenêtre d'instruction centralisée ou décentralisée, et non des ''scoreboard''. Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps, là encore en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire peut émettre 8 µops à la fois, on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle. ===L'unité de renommage superscalaire=== Avec la superscalarité, l'unité de renommage de registre doit gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance de registre, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées. Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat. [[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]] Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante. [[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]] Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande. [[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]] ===Le banc de registre d'un processeur superscalaire=== Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement. 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. ==L'évolution historique des processeurs superscalaires== Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs triple émission permettent eux d’émettre trois instructions à la fois. Les tout premiers processeurs superscalaires avaient un budget en transistors très limité, ce qui fait qu'ils ne pouvaient pas être "totalement superscalaires". Par exemple, un processeur double émission ne pouvait pas émettre deux opérations entières en même temps, ni deux opérations flottantes, ni une opération entière avec un accès mémoire. Il y avait des paires d'instructions autorisées et des paires interdites. Les '''contraintes d'appariement''' sur les instructions à émettre en même temps étaient très strictes. L'intérêt de ces contraintes d’appariement est que le processeur était très simple, il n'y avait pas besoin de rajouter des circuits complexes pour rendre le processeur superscalaire. Les CPU superscalaires historiques ne faisaient que mieux utiliser des unités de calcul déjà présentes. Prenons un processeur non-superscalaire simple, illustré ci-dessous. Il contient : une unité de chargement, un décodeur, une unité d'émission, une ALU, une FPU, les bancs de registre entier et flottants, et une unité mémoire. Nous avons placé l'unité mémoire à la suite de l'ALU entière, car ce sera important pour les explications qui vont suivre. [[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]] Les processeurs superscalaires historiques modifiaient assez peu le schéma précédent. L'unité de chargement était adaptée pour lire plusieurs instructions depuis la mémoire, il y avait un seul décodeur, l'unité d'émission était assez peu modifiée. Mais une chose était certaine : aucune unité de calcul n'était dupliquée, le banc de registre n'était presque pas modifié. A la rigueur, gérer la triple 'émission avec les branchements demandait parfois d'ajouter des ports de lecture sur le banc de registre généraux, pas plus. ===La double émission entière-flottante=== Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est d'émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Le processeur a donc deux pipelines séparés : un pour les micro-opérations entières, un autre pour les micro-opérations flottantes. On parle alors de '''double émission entière-flottante'''. {|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, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité. La plupart des premiers processeurs superscalaires étaient de ce type. Les exemples les plus notables sont les processeurs POWER 1 et ses dérivés comme le ''RISC Single Chip''. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle. [[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]] La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisations visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate]. ===L'émission parallèle des branchements=== Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur. 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 modifier le banc de registres. Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. [[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]] Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications et les divisions ? Pour étudier ces questions, nous allons prendre le cas d'un processeur capable d’émettre trois opérations en même temps : deux opérations entières, et une flottante, potentiellement d'autres. Le cas des opérations complexes est assez intéressant. Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les ALUs en N exemplaires. Mais ce n'est jamais fait en pratique. Concrètement, seules les ALU entières simples le sont, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ne sont pas dupliqués en N exemplaires, le cout en transistors serait trop important. Pour un processeur à double émission, l'unité d'émission a deux ports d'émission, pour émettre deux µops entières. Les deux ports ont chacun leur ALU entière, mais l'un d'entre eux a est aussi relié au circuit multiplier. En clair, cela permet d'émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Idem pour les opérations simples comme les soustractions, les opérations bit à bit, etc. On peut en exécuter deux, ou en coupler une avec une multiplication. Faire ainsi n'a pas de cout en performance significatif, comparé à une duplication totale des ALUs entières. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur serait inutile. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle. [[File:Emission multiple des opérations entières, implémentation naive.png|centre|vignette|upright=2|Émission multiple des opérations entières, implémentation naïve.]] Il en est de même avec les décalages, vu que le ''barrel shifter'' n'est pas dupliqué. Il est donc possible d'émettre deux opérations de base, un décalage avec une opération de base, mais pas deux décalages en parallèle. En général, le ''barrel shifter'' et le multiplieur sont placés sur deux ports d'émission séparés. Cela permet ainsi de faire un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas. [[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Emission multiple des opérations entières, implémentation courante]] Pour les branchements, deux solutions sont possibles. Avec la première, il y a émission parallèle des branchements, à savoir qu'il est possible d'émettre en branchement en parallèle de deux-trois instructions entières. Et cela impose la présence d'une unité de branchement séparée, avec son port d'émission. Avec la seconde, les branchements sont traités comme des instructions entières. Il peut y avoir une unité de branchement séparée, mais ce n'est pas souvent le cas. En général, les branchements sont exécutés dans une ALU entière. [[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Emission série et parallèle des branchements]] Les accès mémoire sont traités comme des opérations entières un peu spéciales, sur les processeurs superscalaires historiques. En clair, le processeur peut émettre deux opérations entières, ou une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent. Les raisons à cela étaient nombreuses. Et ces raisons sont encore valables sur des petits processeurs destinés à l'embarqué ou l'informatique industrielle. Seuls les processeurs haute performance modernes font différemment. La raison principale est le cout en circuit. Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse. Pas besoin de rajouter une unité de calcul d'adresse séparée. Mais surtout, permettre l'émission parallèle des accès mémoire ferait passer un processeur triple émission à de la quadruple émission, avec tous les couts que ça implique. Il faudrait rajouter des ports de lecture/écriture au banc de registre généraux, pour traiter un accès mémoire en plus du reste. Les circuits d'émission et de chargement devraient être élargit, aussi. Et quitte à payer un tel cout en circuit, il serait plus rentable de l'utiliser pour émettre une quatrième opération entière qu'un accès mémoire. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand-chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2. ===Les CPU superscalaires à plusieurs unités spécialisées=== Le classique deux opérations entières + une flottante était l'idéal pour la triple émission. Même les processeurs modernes à triple émission restent sur cette "norme", car elle est proche de l'optimal. Mais arrivé à la quadruple émission, les possibilités sont devenues plus diverses. L'évolution des CPU superscalaires ont évolués dans des directions différentes, chacun faisant à sa sauce, les choses sont devenues plus confuses. Le seul point commun est qu'ils ont eu tendance à rajouter des unités de calcul. Un point important est que la quadruple émission permet de nombreuses possibilités, mais la plupart ne sont pas intéressantes. Pour donner un exemple concret, prenons un CPU superscalaire très simple, comprenant une ALU entière, une FPU, une unité de branchement, et une unité mémoire. Il est possible de créer un CPU quadruple émission avec ces unités, mais le gain en performance n'en vaudrait pas la peine pour le cout en circuit. Il est rare qu'un bloc de 4 instructions contienne exactement une opération entière, une flottante, un branchement et un accès mémoire. En pratique, les seules combinaisons fréquentes sont les combinaisons calcul + accès mémoire et calcul + branchement. Les combinaisons calcul entier + calcul flottant sont aussi possibles, mais plus rares. Mais on dépasse rarement la double émission. Il est préférable d'utiliser ces unités dans un CPU double ou triple émission, en retirant l'unité de branchement et/ou en couplant l'unité mémoire à l'ALU. En clair, une fois arrivé à la quadruple émission, on ne peut plus se contenter d'une ALU, d'une FPU, d'une unité de branchement et d'une unité mémoire. Les processeurs superscalaires larges n'ont pas le choix que de rajouter des unités de calcul, voire des unités mémoire. Et en pratique, ils ont privilégié l'ajout d'ALU entières, avant de dupliquer les autres circuits. La raison est que les programmes informatiques utilisent beaucoup plus d'additions/soustractions/comparaisons que le reste. Il y a environ 5 opérations entières de base pour une multiplication entière, le ratio est le même quand on remplace la multiplication par un branchement ou un accès mémoire. En conséquence, les ALU entières ont été dupliquées en premier, et dans de nombreux exemplaires. Reste à voir comment a été dupliqué le reste. Les multiplieurs n'ont pas été dupliqués, sauf sur les processeurs superscalaires très larges. La raison est qu'il est rare de devoir exécuter deux multiplications à la fois. C'est la même chose pour le ''barrel shifter''. Deux branchements consécutifs sont quant à eux plus fréquents, c'est même assez courant. Aussi, les unités de branchement ont été dupliquées en second lieu, après les ALU entières. Avoir plusieurs accès mémoire proches est assez courant, surtout sur les CPU de type CISC. Aussi, les unités mémoire ont été dupliquées en troisième lieu. Pour résumer, dupliquer des ALU entières est donc la priorité, suivi par la duplication des unités de branchement, puis celle des accès mémoire, pour terminer par le multiplieur. La FPU est un peu à part. Une autre tendance a été de séparer l'unité mémoire des ALU entières, ce qui a permis l''''émission parallèle des µops mémoire'''. Comme son nom l'indique, l'idée est de séparer les µops mémoire des opérations entières. Il devient alors possible d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière. La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple. La '''double émission flottante''' a été utilisée sur les processeurs Alpha. L'idée est d'émettre deux opérations flottantes à la fois. Sauf qu'une implémentation naïve demanderait de dupliquer la FPU, ce qui aurait un cout en circuit rédhibitoire. Pour rappel, les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant. L'idée est d'avoir des ports d'émission séparés pour l'additionneur flottant et le multiplieur flottant. Le processeur peut alors émettre une addition flottante en même temps qu'une multiplication flottante. Les autres circuits de calcul flottant sont répartis sur ces deux ports d'émission flottants. L'avantage est que cela se marie bien avec l'usage d'une fenêtre d'instruction séparée pour les opérations flottantes. La fenêtre d'instruction a alors deux ports séparés, au lieu d'un seul. Rajouter un second port d'émission flottant n'est pas trop un problème, car le cout lié à l'ajout d'un port n'est pas linéaire. Passer de un port à deux a un cout tolérable, bien plus que de passer de 3 ports à 4 ou de 4 à 5. Il est possible de dupliquer l'additionneur et le multiplieur flottant, ce qui permet d'émettre deux additions et multiplications flottantes en même temps. C'est ce qui est fait sur les processeurs AMD d'architecture Zen 1 et 2. Ils ont deux additionneurs flottants par cœur, deux multiplieurs flottants, chacun avec leur propre port d'émission. Les performances en calcul flottant sont assez impressionnantes pour un processeur de l'époque. [[File:ZEN - émission multiple flottante.png|centre|vignette|upright=2|Microarchitecture Zen 1 d'AMD.]] ==Les ports d'émission et de décodage== Les ports d'émission et de décodage sont des ressources matérielles comme les autres. Dans cette section, nous allons parler de concepts liés aux ports d'émission et de décodage. Nous allons notamment parler du partage des ports d'émission, ainsi que des relations numériques entre ces deux types de ports. ===Le partage des ports d'émission et la ''port contention''=== Il est possible de partager un port d'émission, à savoir de connecter un port d'émission à plusieurs unités de calcul. Nous en avons vu un exemple plus haut, avec l'émission multiple des opérations entières. On peut par exemple connecter un multiplieur et une ALU entière sur le même port d'émission. Pour le moment, nous n'avons vu que des cas où un port d'émission est relié soit à des ALU entières, soit à des FPU, pas aux deux. Mais il existe des processeurs où un même port d'émission alimente à la fois une ALU entière et une FPU. Par exemple, on peut relier un additionneur flottant sur le même port qu'une ALU entière. Il faut noter que cela implique une fenêtre d'instruction centralisée, capable de mettre en attente micro-opérations entières et flottantes. Un exemple est celui des processeurs Core 2 Duo. Ils disposent de 6 ports d'émission, dont 3 ports dédiés à l'unité mémoire. Les 3 ports restants alimentent chacun : une ALU entière, un circuit de calcul flottant et une unité de calcul SSE (une unité de calcul SIMD, qu'on abordera dans quelques chapitres). * Le premier port alimente une ALU entière simple et un multiplieur/diviseur flottant. * Le second alimente une ALU entière, un multiplieur entier et un additionneur flottant. * Le troisième alimente une ALU entière, sans circuit flottant dédié. Dans le même genre, il est possible de partager un port d'émission entre l'unité mémoire et une ALU entière. Cela permet d'utiliser l'ALU entière pour les calculs d'adresse, ce qui évite d'avoir à utiliser une unité de calcul d'adresse distincte. Un exemple est celui du processeur superscalaire double émission Power PC 440. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. L'organisation en question permet soit d'émettre un accès mémoire en même temps qu'une opération entière, soit d'émettre deux opérations entières simples, soit d’émettre une multiplication et une addition/soustraction/comparaison. Une organisation simple, mais très efficace ! [[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]] Regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles, qui ne se présentent que pour certaines combinaisons de micro-opérations bien précises. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port. Le nombre de ports peut être un facteur limitant pour la performance dans certaines situations de '''''port contention''''' où on a assez d'ALU pour exécuter N micro-opérations, mais où la répartition des ALUs sur les ports l’empêche. Mais on peut réduire la ''port contention'' en répartissant correctement les ALU sur les ports d'émission. Le choix en question dépendent fortement du fait qu'en moyenne, que certaines paires d'instructions sont plus fréquentes que d'autres. Tout cela guide ce choix de répartition des ALUs sur les ports. Nous venons de voir que la répartition des ALU sur les ports d'émission est très variable d'un processeur à l'autre. Les divers choix possibles sont tous des compromis entre deux forces : réduire le nombre de ports d'émission d'un côté, garder de bonnes performances en réduisant la ''port contention'' de l'autre. Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction relativement simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ? Par contre, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal réparti les ALU. ===La séparation entre largeur de décodage et d'émission=== Sur les processeurs superscalaires larges, le processeur peut émettre plus d'instructions qu'il peut en décoder. Plus haut, j'ai dit que l'unité d'émission avait parfois plus de ports d'émission que de ports de décodage. La raison est assez simple, et elle s'explique assez bien avec un exemple. Prenons un processeur avec trois ALU entières, une FPU, une unité de branchement et une unité mémoire. Chacune a son propre port d'émission, ce qui fait que le CPU est capable d'émettre 6 µops. Du moins, c'est la théorie, car les conditions pour sont assez rares. Il faut qu'en chargeant un paquet de 6 instructions, il y ait exactement trois instructions entières, une flottante, un branchement et un accès mémoire. Si le paquet contient 5 instructions entières et un branchement, seules quatre instructions sont émises. Dans cet exemple, dans la grande majorité des cas, seuls 4 ports seront utilisés en moyenne et deux seront sous-utilisés. Il s'agit là d'une moyenne, qui dépend du code, et des instructions exactes à exécuter. Cependant, cette moyenne nous donne un moyen d'économiser beaucoup de circuits pour un cout en performance mineur. L'idée est de ne charger que des blocs de 4 instructions, mais d'avoir 6 ports d'émission. Le processeur charge et décode 4 instructions, les quatre instructions sont envoyées sur les ports d'émission adéquat. Le cout en transistors est alors situé entre celui d'un CPU quadruple émission et sextuple émission. Et les performances sont aussi entre les deux. Pour les performances, c'est parce que cela permet d'avoir un CPU quadruple émission avec bien moins de contraintes d’appariement. Imaginez qu'on se soit limité à quatre ports d'émission : il aurait fallu mettre plusieurs unités sur le même port d'émission. En conséquence, il y aurait eu des combinaisons interdites, comme émettre deux instructions entières + un branchement + un accès mémoire ; ou deux instructions entières + une opération flottante + un accès mémoire. De tells appariement interdits sont possibles en utilisant les six ports d'émission. ==L'émission multiple des accès mémoire== Les premiers processeurs superscalaires ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles. Par contre, c'est autre chose sur les processeurs superscalaires larges. Vu qu'ils travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares. L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. La ''load/store queue'' reste une structure unique, mais on peut lui rajouter des ports. Et à ce peit jeu, il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire. La première duplique les unités de calcul, mais pas les ports d'accès au cache. Les accès mémoire sont alors accumulés dans la ''load/store queue'', qui sont ensuite traités un par un par le cache. Un processeur utilisant cette technique est, par exemple, capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. Un exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. Ces processeurs pouvaient calculer trois adresses simultanément, mais ne pouvaient faire qu'un seul accès au cache par cycle. Le cout en circuit est modéré : les unités de calcul coutent très peu, les ports de la ''load/store queue'' coutent déjà un peu plus. La seconde méthode ajoute des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultanées. Il faut alors dupliquer des unités de calcul, rajouter les ports sur le cache, mais aussi rajouter des ports sur la ''load/store queue'', pour qu'elle échange des données/commandes mémoire avec le cache. Le cout en circuits est alors conséquent Quelques processeurs utilisaient des solutions intermédiaires, un exemple classique étant les Pentium 1 et 2 d'Intel. Mais nous en parlerons dans le chapitre suivant. [[File:Processeur non-superscalaire basique 02.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]] Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre. Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Le Pentium 4 était dans ce cas, mais il était un peu seul dans sa catégorie. Un cas similaire est celui où l'unité mémoire est alimentée par une fenêtre de micro-opération mélangeant instructions mémoire et entières. Les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. Dans ce cas, l'émission multiple est rarement implémentée. Le cas suivant a une file pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique. Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui pouvaient seulement émettre une lecture en même temps qu'une écriture. Ils avaient deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse. [[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]] Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture demande d'utiliser une file d'écriture multiport, ce qui est assez compliqué. Implémenter des lectures en plus est plus simple : il suffit de rajouter une seconde file de µops LOAD et une unité de calcul, alimentées via un second port d'émission. Il est aussi possible de rajouter un second port de lecture au cache, mais ce n'est pas obligatoire. Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse. De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports de lecture et d'écriture car il y a plus de lecture que d'écritures. Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. : Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse. [[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]] Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettent plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail. Pour résumer, l'évolution des CPU superscalaires a été la suivante : * pipeline mémorie fusionné avec le pipeline entier ; * séparation du pipeline mémoire du reste, avec émission parallèle des accès mémoire ; * émission multiple, mais sans dupliquer les ports du cache ; * émission multiple, avec émission simultanée d'une lecture et d’une écriture dans le cache ; * émission multiple avec plusieurs accès en lecture au cache. ==Les optimisations des processeurs superscalaires larges== Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul. ===L'étape de chargement d'un CPU superscalaire large=== Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 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 autoaligné.]] Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements ! [[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]] Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez 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 microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total). Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois 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 microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement. Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations. ===La micro-fusion des processeurs Intel et AMD=== La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''. Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''. Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout. La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Le parallélisme mémoire | prevText=Le parallélisme mémoire | next=Exemples de microarchitectures CPU : le cas du x86 | nextText=Exemples de microarchitectures CPU : le cas du x86 }} </noinclude> hbcedatbgn2o9lgvvsm6sbc4u5y5v88 772684 772683 2026-09-21T14:37:15Z Mewtow 31375 /* L'émission multiple des micro-opérations entières */ 772684 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 tout premiers processeurs superscalaires pouvaient émettre deux µops en même temps, ce qui portait le nom de '''double émission'''. Mais le nombre a rapidement augmenté et les processeurs modernes 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. ==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. Par exemple, l'unité de chargement charge deux fois plus d'instructions, mais n'est pas modifiée en profondeur. L'unité d'émission et les circuits d'exécution dans le désordre sont eux aussi altérés. Les unités de calcul sont souvent dupliquées, sauf sur les tout premiers CPU superscalaires. {|class="wikitable" |- ! rowspan="2" | Processeur sans émission multiple | rowspan="2" | Chargement | rowspan="2" | Décodage | rowspan="2" | Renommage | rowspan="2" | Émission | Exécution / ALU | rowspan="2" | ''Commit''/ROB |- | Exécution / ALU |- ! rowspan="4" | Processeur superscalaire | rowspan="4" | Chargement | rowspan="2" | Décodage | rowspan="4" | Renommage | rowspan="4" | Émission | Exécution / ALU | rowspan="4" | ''Commit''/ROB |- | Exécution / ALU |- | rowspan="2" | Décodage | Exécution / ALU |- | Exécution / ALU |} Voyons ce qu'il en est dans le détail. ===Les unités de calcul d'un processeur superscalaire=== Émettre N instructions simultanément demande implicitement d'exécuter N instructions par cycle, voire plus si des instructions multicycles viennent mettre leur grain de sel. Intuitivement, on se dit que les unités de calcul doivent être dupliquées. Et c'est une solution viable pour les processeurs très simples, avec seulement quelques ALU entières. Par exemple, un processeur RISC basique avec seulement une ALU entière et un ''barrel shifter'' peut être rendu superscalaire en dupliquant les deux circuits. Le problème, est que cette solution marche assez mal dès que le processeur contient des circuits multiplieurs et/ou une FPU. Par exemple, un processeur à double émission devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, etc. 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 ! Par contre, l'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission. Un autre cas extrême est celui où aucune unité de calcul n'est dupliquée. Pour comprendre comment c'est possible rappelez-vous qu'un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et éventuellement une unité de branchement. Cela suffit pour créer un processeur quadruple émission, très limité. Un tel processeur peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire. Le problème est qu'il est rare que l'on ait à exécuter en même temps un calcul entier, un calcul flottant, un accès mémoire et un branchement. La quadruple émission n'est donc que rarement utilisée. En pratique, les seules combinaisons fréquentes sont les combinaisons calcul + accès mémoire et calcul + branchement. Les combinaisons calcul entier + calcul flottant sont aussi possibles, mais plus rares. Mais on dépasse rarement la double émission. En clair, à partir de la quadruple émission, on ne peut plus se contenter d'une ALU, d'une FPU, d'une unité de branchement et d'une unité mémoire. Les processeurs superscalaires larges n'ont pas le choix que de rajouter des unités de calcul. Reste à voir ce qui est dupliqué en pratique. 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. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant. La conséquence est que les processeurs superscalaires ont des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a des paires d'instructions autorisées et des paires interdites. Par exemple, l'exécution simultanée de deux branchements est interdite, les branchements sont exécutés l'un après l'autre. Mais il est possible d'émettre un branchement en même temps qu'une autre instruction, en espérant que la prédiction de branchement ait fait une bonne prédiction. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur. En pratique, ces contraintes d’appariement ont peu d’impact. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière, le ratio est le même quand on remplace la multiplication par un branchement ou un accès mémoire. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Deux branchements consécutifs sont quant à eux plus fréquents, idem pour deux accès mémoire. Dupliquer des ALU entières est donc la priorité, suivi par la duplication des unités de branchement, puis celle des accès mémoire, pour terminer par le multiplieur et la FPU. ===Le chargement de plusieurs instructions consécutives=== Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large. Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence. Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas. Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement non-pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc. [[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]] ===Le décodage parallèle des instructions=== Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits. Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur. Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! ===L'unité d'émission d'un processeur superscalaire=== La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. Nous verrons pourquoi dans la suite, mais on peut dire que c'est juste plus simple et plus efficace ainsi. Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop candidate, sous entendu "candidate à l'émission". L'unité d'émission détecte les dépendances entre la µop candidate et les µops en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en cours d'exécution dans le pipeline. mais en plus, elle doit tester les dépendances entre les µops candidates. Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément, il doit détecter les paires d'instructions dépendantes et les sérialiser. Avec une fenêtre d'instruction, il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil gérent les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires modernes utilisent tous une fenêtre d'instruction centralisée ou décentralisée, et non des ''scoreboard''. Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps, là encore en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire peut émettre 8 µops à la fois, on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle. ===L'unité de renommage superscalaire=== Avec la superscalarité, l'unité de renommage de registre doit gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance de registre, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées. Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat. [[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]] Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante. [[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]] Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande. [[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]] ===Le banc de registre d'un processeur superscalaire=== Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement. 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. ==L'évolution historique des processeurs superscalaires== Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs triple émission permettent eux d’émettre trois instructions à la fois. Les tout premiers processeurs superscalaires avaient un budget en transistors très limité, ce qui fait qu'ils ne pouvaient pas être "totalement superscalaires". Par exemple, un processeur double émission ne pouvait pas émettre deux opérations entières en même temps, ni deux opérations flottantes, ni une opération entière avec un accès mémoire. Il y avait des paires d'instructions autorisées et des paires interdites. Les '''contraintes d'appariement''' sur les instructions à émettre en même temps étaient très strictes. L'intérêt de ces contraintes d’appariement est que le processeur était très simple, il n'y avait pas besoin de rajouter des circuits complexes pour rendre le processeur superscalaire. Les CPU superscalaires historiques ne faisaient que mieux utiliser des unités de calcul déjà présentes. Prenons un processeur non-superscalaire simple, illustré ci-dessous. Il contient : une unité de chargement, un décodeur, une unité d'émission, une ALU, une FPU, les bancs de registre entier et flottants, et une unité mémoire. Nous avons placé l'unité mémoire à la suite de l'ALU entière, car ce sera important pour les explications qui vont suivre. [[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]] Les processeurs superscalaires historiques modifiaient assez peu le schéma précédent. L'unité de chargement était adaptée pour lire plusieurs instructions depuis la mémoire, il y avait un seul décodeur, l'unité d'émission était assez peu modifiée. Mais une chose était certaine : aucune unité de calcul n'était dupliquée, le banc de registre n'était presque pas modifié. A la rigueur, gérer la triple 'émission avec les branchements demandait parfois d'ajouter des ports de lecture sur le banc de registre généraux, pas plus. ===La double émission entière-flottante=== Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est d'émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Le processeur a donc deux pipelines séparés : un pour les micro-opérations entières, un autre pour les micro-opérations flottantes. On parle alors de '''double émission entière-flottante'''. {|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, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité. La plupart des premiers processeurs superscalaires étaient de ce type. Les exemples les plus notables sont les processeurs POWER 1 et ses dérivés comme le ''RISC Single Chip''. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle. [[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]] La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisations visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate]. ===L'émission parallèle des branchements=== Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur. 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 modifier le banc de registres. Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. [[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]] Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications et les divisions ? Pour étudier ces questions, nous allons prendre le cas d'un processeur capable d’émettre trois opérations en même temps : deux opérations entières, et une flottante, potentiellement d'autres. {|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 |} Le cas des opérations complexes est assez intéressant. Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les ALUs en N exemplaires. Mais ce n'est jamais fait en pratique. Concrètement, seules les ALU entières simples le sont, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ne sont pas dupliqués en N exemplaires, le cout en transistors serait trop important. Pour un processeur à double émission, l'unité d'émission a deux ports d'émission, pour émettre deux µops entières. Les deux ports ont chacun leur ALU entière, mais l'un d'entre eux a est aussi relié au circuit multiplier. En clair, cela permet d'émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Idem pour les opérations simples comme les soustractions, les opérations bit à bit, etc. On peut en exécuter deux, ou en coupler une avec une multiplication. Faire ainsi n'a pas de cout en performance significatif, comparé à une duplication totale des ALUs entières. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur serait inutile. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle. [[File:Emission multiple des opérations entières, implémentation naive.png|centre|vignette|upright=2|Émission multiple des opérations entières, implémentation naïve.]] Il en est de même avec les décalages, vu que le ''barrel shifter'' n'est pas dupliqué. Il est donc possible d'émettre deux opérations de base, un décalage avec une opération de base, mais pas deux décalages en parallèle. En général, le ''barrel shifter'' et le multiplieur sont placés sur deux ports d'émission séparés. Cela permet ainsi de faire un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas. [[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Emission multiple des opérations entières, implémentation courante]] Pour les branchements, deux solutions sont possibles. Avec la première, il y a émission parallèle des branchements, à savoir qu'il est possible d'émettre en branchement en parallèle de deux-trois instructions entières. Et cela impose la présence d'une unité de branchement séparée, avec son port d'émission. Avec la seconde, les branchements sont traités comme des instructions entières. Il peut y avoir une unité de branchement séparée, mais ce n'est pas souvent le cas. En général, les branchements sont exécutés dans une ALU entière. [[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Emission série et parallèle des branchements]] Les accès mémoire sont traités comme des opérations entières un peu spéciales, sur les processeurs superscalaires historiques. En clair, le processeur peut émettre deux opérations entières, ou une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent. Les raisons à cela étaient nombreuses. Et ces raisons sont encore valables sur des petits processeurs destinés à l'embarqué ou l'informatique industrielle. Seuls les processeurs haute performance modernes font différemment. La raison principale est le cout en circuit. Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse. Pas besoin de rajouter une unité de calcul d'adresse séparée. Mais surtout, permettre l'émission parallèle des accès mémoire ferait passer un processeur triple émission à de la quadruple émission, avec tous les couts que ça implique. Il faudrait rajouter des ports de lecture/écriture au banc de registre généraux, pour traiter un accès mémoire en plus du reste. Les circuits d'émission et de chargement devraient être élargit, aussi. Et quitte à payer un tel cout en circuit, il serait plus rentable de l'utiliser pour émettre une quatrième opération entière qu'un accès mémoire. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand-chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2. ===Les CPU superscalaires à plusieurs unités spécialisées=== Le classique deux opérations entières + une flottante était l'idéal pour la triple émission. Même les processeurs modernes à triple émission restent sur cette "norme", car elle est proche de l'optimal. Mais arrivé à la quadruple émission, les possibilités sont devenues plus diverses. L'évolution des CPU superscalaires ont évolués dans des directions différentes, chacun faisant à sa sauce, les choses sont devenues plus confuses. Le seul point commun est qu'ils ont eu tendance à rajouter des unités de calcul. Un point important est que la quadruple émission permet de nombreuses possibilités, mais la plupart ne sont pas intéressantes. Pour donner un exemple concret, prenons un CPU superscalaire très simple, comprenant une ALU entière, une FPU, une unité de branchement, et une unité mémoire. Il est possible de créer un CPU quadruple émission avec ces unités, mais le gain en performance n'en vaudrait pas la peine pour le cout en circuit. Il est rare qu'un bloc de 4 instructions contienne exactement une opération entière, une flottante, un branchement et un accès mémoire. En pratique, les seules combinaisons fréquentes sont les combinaisons calcul + accès mémoire et calcul + branchement. Les combinaisons calcul entier + calcul flottant sont aussi possibles, mais plus rares. Mais on dépasse rarement la double émission. Il est préférable d'utiliser ces unités dans un CPU double ou triple émission, en retirant l'unité de branchement et/ou en couplant l'unité mémoire à l'ALU. En clair, une fois arrivé à la quadruple émission, on ne peut plus se contenter d'une ALU, d'une FPU, d'une unité de branchement et d'une unité mémoire. Les processeurs superscalaires larges n'ont pas le choix que de rajouter des unités de calcul, voire des unités mémoire. Et en pratique, ils ont privilégié l'ajout d'ALU entières, avant de dupliquer les autres circuits. La raison est que les programmes informatiques utilisent beaucoup plus d'additions/soustractions/comparaisons que le reste. Il y a environ 5 opérations entières de base pour une multiplication entière, le ratio est le même quand on remplace la multiplication par un branchement ou un accès mémoire. En conséquence, les ALU entières ont été dupliquées en premier, et dans de nombreux exemplaires. Reste à voir comment a été dupliqué le reste. Les multiplieurs n'ont pas été dupliqués, sauf sur les processeurs superscalaires très larges. La raison est qu'il est rare de devoir exécuter deux multiplications à la fois. C'est la même chose pour le ''barrel shifter''. Deux branchements consécutifs sont quant à eux plus fréquents, c'est même assez courant. Aussi, les unités de branchement ont été dupliquées en second lieu, après les ALU entières. Avoir plusieurs accès mémoire proches est assez courant, surtout sur les CPU de type CISC. Aussi, les unités mémoire ont été dupliquées en troisième lieu. Pour résumer, dupliquer des ALU entières est donc la priorité, suivi par la duplication des unités de branchement, puis celle des accès mémoire, pour terminer par le multiplieur. La FPU est un peu à part. Une autre tendance a été de séparer l'unité mémoire des ALU entières, ce qui a permis l''''émission parallèle des µops mémoire'''. Comme son nom l'indique, l'idée est de séparer les µops mémoire des opérations entières. Il devient alors possible d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière. La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple. La '''double émission flottante''' a été utilisée sur les processeurs Alpha. L'idée est d'émettre deux opérations flottantes à la fois. Sauf qu'une implémentation naïve demanderait de dupliquer la FPU, ce qui aurait un cout en circuit rédhibitoire. Pour rappel, les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant. L'idée est d'avoir des ports d'émission séparés pour l'additionneur flottant et le multiplieur flottant. Le processeur peut alors émettre une addition flottante en même temps qu'une multiplication flottante. Les autres circuits de calcul flottant sont répartis sur ces deux ports d'émission flottants. L'avantage est que cela se marie bien avec l'usage d'une fenêtre d'instruction séparée pour les opérations flottantes. La fenêtre d'instruction a alors deux ports séparés, au lieu d'un seul. Rajouter un second port d'émission flottant n'est pas trop un problème, car le cout lié à l'ajout d'un port n'est pas linéaire. Passer de un port à deux a un cout tolérable, bien plus que de passer de 3 ports à 4 ou de 4 à 5. Il est possible de dupliquer l'additionneur et le multiplieur flottant, ce qui permet d'émettre deux additions et multiplications flottantes en même temps. C'est ce qui est fait sur les processeurs AMD d'architecture Zen 1 et 2. Ils ont deux additionneurs flottants par cœur, deux multiplieurs flottants, chacun avec leur propre port d'émission. Les performances en calcul flottant sont assez impressionnantes pour un processeur de l'époque. [[File:ZEN - émission multiple flottante.png|centre|vignette|upright=2|Microarchitecture Zen 1 d'AMD.]] ==Les ports d'émission et de décodage== Les ports d'émission et de décodage sont des ressources matérielles comme les autres. Dans cette section, nous allons parler de concepts liés aux ports d'émission et de décodage. Nous allons notamment parler du partage des ports d'émission, ainsi que des relations numériques entre ces deux types de ports. ===Le partage des ports d'émission et la ''port contention''=== Il est possible de partager un port d'émission, à savoir de connecter un port d'émission à plusieurs unités de calcul. Nous en avons vu un exemple plus haut, avec l'émission multiple des opérations entières. On peut par exemple connecter un multiplieur et une ALU entière sur le même port d'émission. Pour le moment, nous n'avons vu que des cas où un port d'émission est relié soit à des ALU entières, soit à des FPU, pas aux deux. Mais il existe des processeurs où un même port d'émission alimente à la fois une ALU entière et une FPU. Par exemple, on peut relier un additionneur flottant sur le même port qu'une ALU entière. Il faut noter que cela implique une fenêtre d'instruction centralisée, capable de mettre en attente micro-opérations entières et flottantes. Un exemple est celui des processeurs Core 2 Duo. Ils disposent de 6 ports d'émission, dont 3 ports dédiés à l'unité mémoire. Les 3 ports restants alimentent chacun : une ALU entière, un circuit de calcul flottant et une unité de calcul SSE (une unité de calcul SIMD, qu'on abordera dans quelques chapitres). * Le premier port alimente une ALU entière simple et un multiplieur/diviseur flottant. * Le second alimente une ALU entière, un multiplieur entier et un additionneur flottant. * Le troisième alimente une ALU entière, sans circuit flottant dédié. Dans le même genre, il est possible de partager un port d'émission entre l'unité mémoire et une ALU entière. Cela permet d'utiliser l'ALU entière pour les calculs d'adresse, ce qui évite d'avoir à utiliser une unité de calcul d'adresse distincte. Un exemple est celui du processeur superscalaire double émission Power PC 440. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. L'organisation en question permet soit d'émettre un accès mémoire en même temps qu'une opération entière, soit d'émettre deux opérations entières simples, soit d’émettre une multiplication et une addition/soustraction/comparaison. Une organisation simple, mais très efficace ! [[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]] Regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles, qui ne se présentent que pour certaines combinaisons de micro-opérations bien précises. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port. Le nombre de ports peut être un facteur limitant pour la performance dans certaines situations de '''''port contention''''' où on a assez d'ALU pour exécuter N micro-opérations, mais où la répartition des ALUs sur les ports l’empêche. Mais on peut réduire la ''port contention'' en répartissant correctement les ALU sur les ports d'émission. Le choix en question dépendent fortement du fait qu'en moyenne, que certaines paires d'instructions sont plus fréquentes que d'autres. Tout cela guide ce choix de répartition des ALUs sur les ports. Nous venons de voir que la répartition des ALU sur les ports d'émission est très variable d'un processeur à l'autre. Les divers choix possibles sont tous des compromis entre deux forces : réduire le nombre de ports d'émission d'un côté, garder de bonnes performances en réduisant la ''port contention'' de l'autre. Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction relativement simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ? Par contre, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal réparti les ALU. ===La séparation entre largeur de décodage et d'émission=== Sur les processeurs superscalaires larges, le processeur peut émettre plus d'instructions qu'il peut en décoder. Plus haut, j'ai dit que l'unité d'émission avait parfois plus de ports d'émission que de ports de décodage. La raison est assez simple, et elle s'explique assez bien avec un exemple. Prenons un processeur avec trois ALU entières, une FPU, une unité de branchement et une unité mémoire. Chacune a son propre port d'émission, ce qui fait que le CPU est capable d'émettre 6 µops. Du moins, c'est la théorie, car les conditions pour sont assez rares. Il faut qu'en chargeant un paquet de 6 instructions, il y ait exactement trois instructions entières, une flottante, un branchement et un accès mémoire. Si le paquet contient 5 instructions entières et un branchement, seules quatre instructions sont émises. Dans cet exemple, dans la grande majorité des cas, seuls 4 ports seront utilisés en moyenne et deux seront sous-utilisés. Il s'agit là d'une moyenne, qui dépend du code, et des instructions exactes à exécuter. Cependant, cette moyenne nous donne un moyen d'économiser beaucoup de circuits pour un cout en performance mineur. L'idée est de ne charger que des blocs de 4 instructions, mais d'avoir 6 ports d'émission. Le processeur charge et décode 4 instructions, les quatre instructions sont envoyées sur les ports d'émission adéquat. Le cout en transistors est alors situé entre celui d'un CPU quadruple émission et sextuple émission. Et les performances sont aussi entre les deux. Pour les performances, c'est parce que cela permet d'avoir un CPU quadruple émission avec bien moins de contraintes d’appariement. Imaginez qu'on se soit limité à quatre ports d'émission : il aurait fallu mettre plusieurs unités sur le même port d'émission. En conséquence, il y aurait eu des combinaisons interdites, comme émettre deux instructions entières + un branchement + un accès mémoire ; ou deux instructions entières + une opération flottante + un accès mémoire. De tells appariement interdits sont possibles en utilisant les six ports d'émission. ==L'émission multiple des accès mémoire== Les premiers processeurs superscalaires ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles. Par contre, c'est autre chose sur les processeurs superscalaires larges. Vu qu'ils travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares. L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. La ''load/store queue'' reste une structure unique, mais on peut lui rajouter des ports. Et à ce peit jeu, il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire. La première duplique les unités de calcul, mais pas les ports d'accès au cache. Les accès mémoire sont alors accumulés dans la ''load/store queue'', qui sont ensuite traités un par un par le cache. Un processeur utilisant cette technique est, par exemple, capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. Un exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. Ces processeurs pouvaient calculer trois adresses simultanément, mais ne pouvaient faire qu'un seul accès au cache par cycle. Le cout en circuit est modéré : les unités de calcul coutent très peu, les ports de la ''load/store queue'' coutent déjà un peu plus. La seconde méthode ajoute des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultanées. Il faut alors dupliquer des unités de calcul, rajouter les ports sur le cache, mais aussi rajouter des ports sur la ''load/store queue'', pour qu'elle échange des données/commandes mémoire avec le cache. Le cout en circuits est alors conséquent Quelques processeurs utilisaient des solutions intermédiaires, un exemple classique étant les Pentium 1 et 2 d'Intel. Mais nous en parlerons dans le chapitre suivant. [[File:Processeur non-superscalaire basique 02.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]] Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre. Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Le Pentium 4 était dans ce cas, mais il était un peu seul dans sa catégorie. Un cas similaire est celui où l'unité mémoire est alimentée par une fenêtre de micro-opération mélangeant instructions mémoire et entières. Les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. Dans ce cas, l'émission multiple est rarement implémentée. Le cas suivant a une file pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique. Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui pouvaient seulement émettre une lecture en même temps qu'une écriture. Ils avaient deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse. [[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]] Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture demande d'utiliser une file d'écriture multiport, ce qui est assez compliqué. Implémenter des lectures en plus est plus simple : il suffit de rajouter une seconde file de µops LOAD et une unité de calcul, alimentées via un second port d'émission. Il est aussi possible de rajouter un second port de lecture au cache, mais ce n'est pas obligatoire. Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse. De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports de lecture et d'écriture car il y a plus de lecture que d'écritures. Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. : Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse. [[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]] Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettent plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail. Pour résumer, l'évolution des CPU superscalaires a été la suivante : * pipeline mémorie fusionné avec le pipeline entier ; * séparation du pipeline mémoire du reste, avec émission parallèle des accès mémoire ; * émission multiple, mais sans dupliquer les ports du cache ; * émission multiple, avec émission simultanée d'une lecture et d’une écriture dans le cache ; * émission multiple avec plusieurs accès en lecture au cache. ==Les optimisations des processeurs superscalaires larges== Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul. ===L'étape de chargement d'un CPU superscalaire large=== Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 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 autoaligné.]] Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements ! [[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]] Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez 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 microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total). Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois 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 microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement. Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations. ===La micro-fusion des processeurs Intel et AMD=== La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''. Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''. Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout. La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Le parallélisme mémoire | prevText=Le parallélisme mémoire | next=Exemples de microarchitectures CPU : le cas du x86 | nextText=Exemples de microarchitectures CPU : le cas du x86 }} </noinclude> mf6f5et1ud8ilfbw3d4t7a2j0b8xzx4 772685 772684 2026-09-21T14:39:11Z Mewtow 31375 /* L'émission multiple des micro-opérations entières */ 772685 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 tout premiers processeurs superscalaires pouvaient émettre deux µops en même temps, ce qui portait le nom de '''double émission'''. Mais le nombre a rapidement augmenté et les processeurs modernes 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. ==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. Par exemple, l'unité de chargement charge deux fois plus d'instructions, mais n'est pas modifiée en profondeur. L'unité d'émission et les circuits d'exécution dans le désordre sont eux aussi altérés. Les unités de calcul sont souvent dupliquées, sauf sur les tout premiers CPU superscalaires. {|class="wikitable" |- ! rowspan="2" | Processeur sans émission multiple | rowspan="2" | Chargement | rowspan="2" | Décodage | rowspan="2" | Renommage | rowspan="2" | Émission | Exécution / ALU | rowspan="2" | ''Commit''/ROB |- | Exécution / ALU |- ! rowspan="4" | Processeur superscalaire | rowspan="4" | Chargement | rowspan="2" | Décodage | rowspan="4" | Renommage | rowspan="4" | Émission | Exécution / ALU | rowspan="4" | ''Commit''/ROB |- | Exécution / ALU |- | rowspan="2" | Décodage | Exécution / ALU |- | Exécution / ALU |} Voyons ce qu'il en est dans le détail. ===Les unités de calcul d'un processeur superscalaire=== Émettre N instructions simultanément demande implicitement d'exécuter N instructions par cycle, voire plus si des instructions multicycles viennent mettre leur grain de sel. Intuitivement, on se dit que les unités de calcul doivent être dupliquées. Et c'est une solution viable pour les processeurs très simples, avec seulement quelques ALU entières. Par exemple, un processeur RISC basique avec seulement une ALU entière et un ''barrel shifter'' peut être rendu superscalaire en dupliquant les deux circuits. Le problème, est que cette solution marche assez mal dès que le processeur contient des circuits multiplieurs et/ou une FPU. Par exemple, un processeur à double émission devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, etc. 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 ! Par contre, l'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission. Un autre cas extrême est celui où aucune unité de calcul n'est dupliquée. Pour comprendre comment c'est possible rappelez-vous qu'un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et éventuellement une unité de branchement. Cela suffit pour créer un processeur quadruple émission, très limité. Un tel processeur peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire. Le problème est qu'il est rare que l'on ait à exécuter en même temps un calcul entier, un calcul flottant, un accès mémoire et un branchement. La quadruple émission n'est donc que rarement utilisée. En pratique, les seules combinaisons fréquentes sont les combinaisons calcul + accès mémoire et calcul + branchement. Les combinaisons calcul entier + calcul flottant sont aussi possibles, mais plus rares. Mais on dépasse rarement la double émission. En clair, à partir de la quadruple émission, on ne peut plus se contenter d'une ALU, d'une FPU, d'une unité de branchement et d'une unité mémoire. Les processeurs superscalaires larges n'ont pas le choix que de rajouter des unités de calcul. Reste à voir ce qui est dupliqué en pratique. 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. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant. La conséquence est que les processeurs superscalaires ont des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a des paires d'instructions autorisées et des paires interdites. Par exemple, l'exécution simultanée de deux branchements est interdite, les branchements sont exécutés l'un après l'autre. Mais il est possible d'émettre un branchement en même temps qu'une autre instruction, en espérant que la prédiction de branchement ait fait une bonne prédiction. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur. En pratique, ces contraintes d’appariement ont peu d’impact. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière, le ratio est le même quand on remplace la multiplication par un branchement ou un accès mémoire. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Deux branchements consécutifs sont quant à eux plus fréquents, idem pour deux accès mémoire. Dupliquer des ALU entières est donc la priorité, suivi par la duplication des unités de branchement, puis celle des accès mémoire, pour terminer par le multiplieur et la FPU. ===Le chargement de plusieurs instructions consécutives=== Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large. Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence. Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas. Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement non-pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc. [[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]] ===Le décodage parallèle des instructions=== Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits. Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur. Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! ===L'unité d'émission d'un processeur superscalaire=== La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. Nous verrons pourquoi dans la suite, mais on peut dire que c'est juste plus simple et plus efficace ainsi. Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop candidate, sous entendu "candidate à l'émission". L'unité d'émission détecte les dépendances entre la µop candidate et les µops en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en cours d'exécution dans le pipeline. mais en plus, elle doit tester les dépendances entre les µops candidates. Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément, il doit détecter les paires d'instructions dépendantes et les sérialiser. Avec une fenêtre d'instruction, il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil gérent les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires modernes utilisent tous une fenêtre d'instruction centralisée ou décentralisée, et non des ''scoreboard''. Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps, là encore en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire peut émettre 8 µops à la fois, on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle. ===L'unité de renommage superscalaire=== Avec la superscalarité, l'unité de renommage de registre doit gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance de registre, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées. Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat. [[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]] Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante. [[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]] Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande. [[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]] ===Le banc de registre d'un processeur superscalaire=== Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement. 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. ==L'évolution historique des processeurs superscalaires== Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs triple émission permettent eux d’émettre trois instructions à la fois. Les tout premiers processeurs superscalaires avaient un budget en transistors très limité, ce qui fait qu'ils ne pouvaient pas être "totalement superscalaires". Par exemple, un processeur double émission ne pouvait pas émettre deux opérations entières en même temps, ni deux opérations flottantes, ni une opération entière avec un accès mémoire. Il y avait des paires d'instructions autorisées et des paires interdites. Les '''contraintes d'appariement''' sur les instructions à émettre en même temps étaient très strictes. L'intérêt de ces contraintes d’appariement est que le processeur était très simple, il n'y avait pas besoin de rajouter des circuits complexes pour rendre le processeur superscalaire. Les CPU superscalaires historiques ne faisaient que mieux utiliser des unités de calcul déjà présentes. Prenons un processeur non-superscalaire simple, illustré ci-dessous. Il contient : une unité de chargement, un décodeur, une unité d'émission, une ALU, une FPU, les bancs de registre entier et flottants, et une unité mémoire. Nous avons placé l'unité mémoire à la suite de l'ALU entière, car ce sera important pour les explications qui vont suivre. [[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]] Les processeurs superscalaires historiques modifiaient assez peu le schéma précédent. L'unité de chargement était adaptée pour lire plusieurs instructions depuis la mémoire, il y avait un seul décodeur, l'unité d'émission était assez peu modifiée. Mais une chose était certaine : aucune unité de calcul n'était dupliquée, le banc de registre n'était presque pas modifié. A la rigueur, gérer la triple 'émission avec les branchements demandait parfois d'ajouter des ports de lecture sur le banc de registre généraux, pas plus. ===La double émission entière-flottante=== Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est d'émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Le processeur a donc deux pipelines séparés : un pour les micro-opérations entières, un autre pour les micro-opérations flottantes. On parle alors de '''double émission entière-flottante'''. {|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, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité. La plupart des premiers processeurs superscalaires étaient de ce type. Les exemples les plus notables sont les processeurs POWER 1 et ses dérivés comme le ''RISC Single Chip''. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle. [[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]] La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisations visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate]. ===L'émission parallèle des branchements=== Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur. 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 modifier le banc de registres. Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. [[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]] Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications et les divisions ? Pour étudier ces questions, nous allons prendre le cas d'un processeur capable d’émettre trois opérations en même temps : deux opérations entières, et une flottante, potentiellement d'autres. {|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 |} 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|Emission multiple des opérations entières, implémentation courante]] Pour les branchements, deux solutions sont possibles. Avec la première, il y a émission parallèle des branchements, à savoir qu'il est possible d'émettre en branchement en parallèle de deux-trois instructions entières. Et cela impose la présence d'une unité de branchement séparée, avec son port d'émission. Avec la seconde, les branchements sont traités comme des instructions entières. Il peut y avoir une unité de branchement séparée, mais ce n'est pas souvent le cas. En général, les branchements sont exécutés dans une ALU entière. [[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Emission série et parallèle des branchements]] Les accès mémoire sont traités comme des opérations entières un peu spéciales, sur les processeurs superscalaires historiques. En clair, le processeur peut émettre deux opérations entières, ou une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent. Les raisons à cela étaient nombreuses. Et ces raisons sont encore valables sur des petits processeurs destinés à l'embarqué ou l'informatique industrielle. Seuls les processeurs haute performance modernes font différemment. La raison principale est le cout en circuit. Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse. Pas besoin de rajouter une unité de calcul d'adresse séparée. Mais surtout, permettre l'émission parallèle des accès mémoire ferait passer un processeur triple émission à de la quadruple émission, avec tous les couts que ça implique. Il faudrait rajouter des ports de lecture/écriture au banc de registre généraux, pour traiter un accès mémoire en plus du reste. Les circuits d'émission et de chargement devraient être élargit, aussi. Et quitte à payer un tel cout en circuit, il serait plus rentable de l'utiliser pour émettre une quatrième opération entière qu'un accès mémoire. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand-chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2. ===Les CPU superscalaires à plusieurs unités spécialisées=== Le classique deux opérations entières + une flottante était l'idéal pour la triple émission. Même les processeurs modernes à triple émission restent sur cette "norme", car elle est proche de l'optimal. Mais arrivé à la quadruple émission, les possibilités sont devenues plus diverses. L'évolution des CPU superscalaires ont évolués dans des directions différentes, chacun faisant à sa sauce, les choses sont devenues plus confuses. Le seul point commun est qu'ils ont eu tendance à rajouter des unités de calcul. Un point important est que la quadruple émission permet de nombreuses possibilités, mais la plupart ne sont pas intéressantes. Pour donner un exemple concret, prenons un CPU superscalaire très simple, comprenant une ALU entière, une FPU, une unité de branchement, et une unité mémoire. Il est possible de créer un CPU quadruple émission avec ces unités, mais le gain en performance n'en vaudrait pas la peine pour le cout en circuit. Il est rare qu'un bloc de 4 instructions contienne exactement une opération entière, une flottante, un branchement et un accès mémoire. En pratique, les seules combinaisons fréquentes sont les combinaisons calcul + accès mémoire et calcul + branchement. Les combinaisons calcul entier + calcul flottant sont aussi possibles, mais plus rares. Mais on dépasse rarement la double émission. Il est préférable d'utiliser ces unités dans un CPU double ou triple émission, en retirant l'unité de branchement et/ou en couplant l'unité mémoire à l'ALU. En clair, une fois arrivé à la quadruple émission, on ne peut plus se contenter d'une ALU, d'une FPU, d'une unité de branchement et d'une unité mémoire. Les processeurs superscalaires larges n'ont pas le choix que de rajouter des unités de calcul, voire des unités mémoire. Et en pratique, ils ont privilégié l'ajout d'ALU entières, avant de dupliquer les autres circuits. La raison est que les programmes informatiques utilisent beaucoup plus d'additions/soustractions/comparaisons que le reste. Il y a environ 5 opérations entières de base pour une multiplication entière, le ratio est le même quand on remplace la multiplication par un branchement ou un accès mémoire. En conséquence, les ALU entières ont été dupliquées en premier, et dans de nombreux exemplaires. Reste à voir comment a été dupliqué le reste. Les multiplieurs n'ont pas été dupliqués, sauf sur les processeurs superscalaires très larges. La raison est qu'il est rare de devoir exécuter deux multiplications à la fois. C'est la même chose pour le ''barrel shifter''. Deux branchements consécutifs sont quant à eux plus fréquents, c'est même assez courant. Aussi, les unités de branchement ont été dupliquées en second lieu, après les ALU entières. Avoir plusieurs accès mémoire proches est assez courant, surtout sur les CPU de type CISC. Aussi, les unités mémoire ont été dupliquées en troisième lieu. Pour résumer, dupliquer des ALU entières est donc la priorité, suivi par la duplication des unités de branchement, puis celle des accès mémoire, pour terminer par le multiplieur. La FPU est un peu à part. Une autre tendance a été de séparer l'unité mémoire des ALU entières, ce qui a permis l''''émission parallèle des µops mémoire'''. Comme son nom l'indique, l'idée est de séparer les µops mémoire des opérations entières. Il devient alors possible d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière. La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple. La '''double émission flottante''' a été utilisée sur les processeurs Alpha. L'idée est d'émettre deux opérations flottantes à la fois. Sauf qu'une implémentation naïve demanderait de dupliquer la FPU, ce qui aurait un cout en circuit rédhibitoire. Pour rappel, les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant. L'idée est d'avoir des ports d'émission séparés pour l'additionneur flottant et le multiplieur flottant. Le processeur peut alors émettre une addition flottante en même temps qu'une multiplication flottante. Les autres circuits de calcul flottant sont répartis sur ces deux ports d'émission flottants. L'avantage est que cela se marie bien avec l'usage d'une fenêtre d'instruction séparée pour les opérations flottantes. La fenêtre d'instruction a alors deux ports séparés, au lieu d'un seul. Rajouter un second port d'émission flottant n'est pas trop un problème, car le cout lié à l'ajout d'un port n'est pas linéaire. Passer de un port à deux a un cout tolérable, bien plus que de passer de 3 ports à 4 ou de 4 à 5. Il est possible de dupliquer l'additionneur et le multiplieur flottant, ce qui permet d'émettre deux additions et multiplications flottantes en même temps. C'est ce qui est fait sur les processeurs AMD d'architecture Zen 1 et 2. Ils ont deux additionneurs flottants par cœur, deux multiplieurs flottants, chacun avec leur propre port d'émission. Les performances en calcul flottant sont assez impressionnantes pour un processeur de l'époque. [[File:ZEN - émission multiple flottante.png|centre|vignette|upright=2|Microarchitecture Zen 1 d'AMD.]] ==Les ports d'émission et de décodage== Les ports d'émission et de décodage sont des ressources matérielles comme les autres. Dans cette section, nous allons parler de concepts liés aux ports d'émission et de décodage. Nous allons notamment parler du partage des ports d'émission, ainsi que des relations numériques entre ces deux types de ports. ===Le partage des ports d'émission et la ''port contention''=== Il est possible de partager un port d'émission, à savoir de connecter un port d'émission à plusieurs unités de calcul. Nous en avons vu un exemple plus haut, avec l'émission multiple des opérations entières. On peut par exemple connecter un multiplieur et une ALU entière sur le même port d'émission. Pour le moment, nous n'avons vu que des cas où un port d'émission est relié soit à des ALU entières, soit à des FPU, pas aux deux. Mais il existe des processeurs où un même port d'émission alimente à la fois une ALU entière et une FPU. Par exemple, on peut relier un additionneur flottant sur le même port qu'une ALU entière. Il faut noter que cela implique une fenêtre d'instruction centralisée, capable de mettre en attente micro-opérations entières et flottantes. Un exemple est celui des processeurs Core 2 Duo. Ils disposent de 6 ports d'émission, dont 3 ports dédiés à l'unité mémoire. Les 3 ports restants alimentent chacun : une ALU entière, un circuit de calcul flottant et une unité de calcul SSE (une unité de calcul SIMD, qu'on abordera dans quelques chapitres). * Le premier port alimente une ALU entière simple et un multiplieur/diviseur flottant. * Le second alimente une ALU entière, un multiplieur entier et un additionneur flottant. * Le troisième alimente une ALU entière, sans circuit flottant dédié. Dans le même genre, il est possible de partager un port d'émission entre l'unité mémoire et une ALU entière. Cela permet d'utiliser l'ALU entière pour les calculs d'adresse, ce qui évite d'avoir à utiliser une unité de calcul d'adresse distincte. Un exemple est celui du processeur superscalaire double émission Power PC 440. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. L'organisation en question permet soit d'émettre un accès mémoire en même temps qu'une opération entière, soit d'émettre deux opérations entières simples, soit d’émettre une multiplication et une addition/soustraction/comparaison. Une organisation simple, mais très efficace ! [[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]] Regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles, qui ne se présentent que pour certaines combinaisons de micro-opérations bien précises. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port. Le nombre de ports peut être un facteur limitant pour la performance dans certaines situations de '''''port contention''''' où on a assez d'ALU pour exécuter N micro-opérations, mais où la répartition des ALUs sur les ports l’empêche. Mais on peut réduire la ''port contention'' en répartissant correctement les ALU sur les ports d'émission. Le choix en question dépendent fortement du fait qu'en moyenne, que certaines paires d'instructions sont plus fréquentes que d'autres. Tout cela guide ce choix de répartition des ALUs sur les ports. Nous venons de voir que la répartition des ALU sur les ports d'émission est très variable d'un processeur à l'autre. Les divers choix possibles sont tous des compromis entre deux forces : réduire le nombre de ports d'émission d'un côté, garder de bonnes performances en réduisant la ''port contention'' de l'autre. Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction relativement simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ? Par contre, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal réparti les ALU. ===La séparation entre largeur de décodage et d'émission=== Sur les processeurs superscalaires larges, le processeur peut émettre plus d'instructions qu'il peut en décoder. Plus haut, j'ai dit que l'unité d'émission avait parfois plus de ports d'émission que de ports de décodage. La raison est assez simple, et elle s'explique assez bien avec un exemple. Prenons un processeur avec trois ALU entières, une FPU, une unité de branchement et une unité mémoire. Chacune a son propre port d'émission, ce qui fait que le CPU est capable d'émettre 6 µops. Du moins, c'est la théorie, car les conditions pour sont assez rares. Il faut qu'en chargeant un paquet de 6 instructions, il y ait exactement trois instructions entières, une flottante, un branchement et un accès mémoire. Si le paquet contient 5 instructions entières et un branchement, seules quatre instructions sont émises. Dans cet exemple, dans la grande majorité des cas, seuls 4 ports seront utilisés en moyenne et deux seront sous-utilisés. Il s'agit là d'une moyenne, qui dépend du code, et des instructions exactes à exécuter. Cependant, cette moyenne nous donne un moyen d'économiser beaucoup de circuits pour un cout en performance mineur. L'idée est de ne charger que des blocs de 4 instructions, mais d'avoir 6 ports d'émission. Le processeur charge et décode 4 instructions, les quatre instructions sont envoyées sur les ports d'émission adéquat. Le cout en transistors est alors situé entre celui d'un CPU quadruple émission et sextuple émission. Et les performances sont aussi entre les deux. Pour les performances, c'est parce que cela permet d'avoir un CPU quadruple émission avec bien moins de contraintes d’appariement. Imaginez qu'on se soit limité à quatre ports d'émission : il aurait fallu mettre plusieurs unités sur le même port d'émission. En conséquence, il y aurait eu des combinaisons interdites, comme émettre deux instructions entières + un branchement + un accès mémoire ; ou deux instructions entières + une opération flottante + un accès mémoire. De tells appariement interdits sont possibles en utilisant les six ports d'émission. ==L'émission multiple des accès mémoire== Les premiers processeurs superscalaires ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles. Par contre, c'est autre chose sur les processeurs superscalaires larges. Vu qu'ils travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares. L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. La ''load/store queue'' reste une structure unique, mais on peut lui rajouter des ports. Et à ce peit jeu, il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire. La première duplique les unités de calcul, mais pas les ports d'accès au cache. Les accès mémoire sont alors accumulés dans la ''load/store queue'', qui sont ensuite traités un par un par le cache. Un processeur utilisant cette technique est, par exemple, capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. Un exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. Ces processeurs pouvaient calculer trois adresses simultanément, mais ne pouvaient faire qu'un seul accès au cache par cycle. Le cout en circuit est modéré : les unités de calcul coutent très peu, les ports de la ''load/store queue'' coutent déjà un peu plus. La seconde méthode ajoute des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultanées. Il faut alors dupliquer des unités de calcul, rajouter les ports sur le cache, mais aussi rajouter des ports sur la ''load/store queue'', pour qu'elle échange des données/commandes mémoire avec le cache. Le cout en circuits est alors conséquent Quelques processeurs utilisaient des solutions intermédiaires, un exemple classique étant les Pentium 1 et 2 d'Intel. Mais nous en parlerons dans le chapitre suivant. [[File:Processeur non-superscalaire basique 02.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]] Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre. Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Le Pentium 4 était dans ce cas, mais il était un peu seul dans sa catégorie. Un cas similaire est celui où l'unité mémoire est alimentée par une fenêtre de micro-opération mélangeant instructions mémoire et entières. Les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. Dans ce cas, l'émission multiple est rarement implémentée. Le cas suivant a une file pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique. Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui pouvaient seulement émettre une lecture en même temps qu'une écriture. Ils avaient deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse. [[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]] Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture demande d'utiliser une file d'écriture multiport, ce qui est assez compliqué. Implémenter des lectures en plus est plus simple : il suffit de rajouter une seconde file de µops LOAD et une unité de calcul, alimentées via un second port d'émission. Il est aussi possible de rajouter un second port de lecture au cache, mais ce n'est pas obligatoire. Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse. De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports de lecture et d'écriture car il y a plus de lecture que d'écritures. Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. : Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse. [[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]] Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettent plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail. Pour résumer, l'évolution des CPU superscalaires a été la suivante : * pipeline mémorie fusionné avec le pipeline entier ; * séparation du pipeline mémoire du reste, avec émission parallèle des accès mémoire ; * émission multiple, mais sans dupliquer les ports du cache ; * émission multiple, avec émission simultanée d'une lecture et d’une écriture dans le cache ; * émission multiple avec plusieurs accès en lecture au cache. ==Les optimisations des processeurs superscalaires larges== Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul. ===L'étape de chargement d'un CPU superscalaire large=== Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 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 autoaligné.]] Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements ! [[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]] Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez 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 microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total). Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois 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 microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement. Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations. ===La micro-fusion des processeurs Intel et AMD=== La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''. Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''. Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout. La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Le parallélisme mémoire | prevText=Le parallélisme mémoire | next=Exemples de microarchitectures CPU : le cas du x86 | nextText=Exemples de microarchitectures CPU : le cas du x86 }} </noinclude> f3jq7m4pyxa2e9sofpn4j6zquwnuilk 772686 772685 2026-09-21T14:41:09Z Mewtow 31375 /* L'émission multiple des micro-opérations entières */ 772686 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 tout premiers processeurs superscalaires pouvaient émettre deux µops en même temps, ce qui portait le nom de '''double émission'''. Mais le nombre a rapidement augmenté et les processeurs modernes 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. ==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. Par exemple, l'unité de chargement charge deux fois plus d'instructions, mais n'est pas modifiée en profondeur. L'unité d'émission et les circuits d'exécution dans le désordre sont eux aussi altérés. Les unités de calcul sont souvent dupliquées, sauf sur les tout premiers CPU superscalaires. {|class="wikitable" |- ! rowspan="2" | Processeur sans émission multiple | rowspan="2" | Chargement | rowspan="2" | Décodage | rowspan="2" | Renommage | rowspan="2" | Émission | Exécution / ALU | rowspan="2" | ''Commit''/ROB |- | Exécution / ALU |- ! rowspan="4" | Processeur superscalaire | rowspan="4" | Chargement | rowspan="2" | Décodage | rowspan="4" | Renommage | rowspan="4" | Émission | Exécution / ALU | rowspan="4" | ''Commit''/ROB |- | Exécution / ALU |- | rowspan="2" | Décodage | Exécution / ALU |- | Exécution / ALU |} Voyons ce qu'il en est dans le détail. ===Les unités de calcul d'un processeur superscalaire=== Émettre N instructions simultanément demande implicitement d'exécuter N instructions par cycle, voire plus si des instructions multicycles viennent mettre leur grain de sel. Intuitivement, on se dit que les unités de calcul doivent être dupliquées. Et c'est une solution viable pour les processeurs très simples, avec seulement quelques ALU entières. Par exemple, un processeur RISC basique avec seulement une ALU entière et un ''barrel shifter'' peut être rendu superscalaire en dupliquant les deux circuits. Le problème, est que cette solution marche assez mal dès que le processeur contient des circuits multiplieurs et/ou une FPU. Par exemple, un processeur à double émission devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, etc. 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 ! Par contre, l'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission. Un autre cas extrême est celui où aucune unité de calcul n'est dupliquée. Pour comprendre comment c'est possible rappelez-vous qu'un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et éventuellement une unité de branchement. Cela suffit pour créer un processeur quadruple émission, très limité. Un tel processeur peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire. Le problème est qu'il est rare que l'on ait à exécuter en même temps un calcul entier, un calcul flottant, un accès mémoire et un branchement. La quadruple émission n'est donc que rarement utilisée. En pratique, les seules combinaisons fréquentes sont les combinaisons calcul + accès mémoire et calcul + branchement. Les combinaisons calcul entier + calcul flottant sont aussi possibles, mais plus rares. Mais on dépasse rarement la double émission. En clair, à partir de la quadruple émission, on ne peut plus se contenter d'une ALU, d'une FPU, d'une unité de branchement et d'une unité mémoire. Les processeurs superscalaires larges n'ont pas le choix que de rajouter des unités de calcul. Reste à voir ce qui est dupliqué en pratique. 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. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant. La conséquence est que les processeurs superscalaires ont des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a des paires d'instructions autorisées et des paires interdites. Par exemple, l'exécution simultanée de deux branchements est interdite, les branchements sont exécutés l'un après l'autre. Mais il est possible d'émettre un branchement en même temps qu'une autre instruction, en espérant que la prédiction de branchement ait fait une bonne prédiction. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur. En pratique, ces contraintes d’appariement ont peu d’impact. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière, le ratio est le même quand on remplace la multiplication par un branchement ou un accès mémoire. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Deux branchements consécutifs sont quant à eux plus fréquents, idem pour deux accès mémoire. Dupliquer des ALU entières est donc la priorité, suivi par la duplication des unités de branchement, puis celle des accès mémoire, pour terminer par le multiplieur et la FPU. ===Le chargement de plusieurs instructions consécutives=== Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large. Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence. Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas. Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement non-pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc. [[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]] ===Le décodage parallèle des instructions=== Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits. Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur. Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! ===L'unité d'émission d'un processeur superscalaire=== La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. Nous verrons pourquoi dans la suite, mais on peut dire que c'est juste plus simple et plus efficace ainsi. Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop candidate, sous entendu "candidate à l'émission". L'unité d'émission détecte les dépendances entre la µop candidate et les µops en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en cours d'exécution dans le pipeline. mais en plus, elle doit tester les dépendances entre les µops candidates. Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément, il doit détecter les paires d'instructions dépendantes et les sérialiser. Avec une fenêtre d'instruction, il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil gérent les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires modernes utilisent tous une fenêtre d'instruction centralisée ou décentralisée, et non des ''scoreboard''. Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps, là encore en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire peut émettre 8 µops à la fois, on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle. ===L'unité de renommage superscalaire=== Avec la superscalarité, l'unité de renommage de registre doit gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance de registre, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées. Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat. [[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]] Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante. [[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]] Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande. [[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]] ===Le banc de registre d'un processeur superscalaire=== Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement. 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. ==L'évolution historique des processeurs superscalaires== Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs triple émission permettent eux d’émettre trois instructions à la fois. Les tout premiers processeurs superscalaires avaient un budget en transistors très limité, ce qui fait qu'ils ne pouvaient pas être "totalement superscalaires". Par exemple, un processeur double émission ne pouvait pas émettre deux opérations entières en même temps, ni deux opérations flottantes, ni une opération entière avec un accès mémoire. Il y avait des paires d'instructions autorisées et des paires interdites. Les '''contraintes d'appariement''' sur les instructions à émettre en même temps étaient très strictes. L'intérêt de ces contraintes d’appariement est que le processeur était très simple, il n'y avait pas besoin de rajouter des circuits complexes pour rendre le processeur superscalaire. Les CPU superscalaires historiques ne faisaient que mieux utiliser des unités de calcul déjà présentes. Prenons un processeur non-superscalaire simple, illustré ci-dessous. Il contient : une unité de chargement, un décodeur, une unité d'émission, une ALU, une FPU, les bancs de registre entier et flottants, et une unité mémoire. Nous avons placé l'unité mémoire à la suite de l'ALU entière, car ce sera important pour les explications qui vont suivre. [[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]] Les processeurs superscalaires historiques modifiaient assez peu le schéma précédent. L'unité de chargement était adaptée pour lire plusieurs instructions depuis la mémoire, il y avait un seul décodeur, l'unité d'émission était assez peu modifiée. Mais une chose était certaine : aucune unité de calcul n'était dupliquée, le banc de registre n'était presque pas modifié. A la rigueur, gérer la triple 'émission avec les branchements demandait parfois d'ajouter des ports de lecture sur le banc de registre généraux, pas plus. ===La double émission entière-flottante=== Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est d'émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Le processeur a donc deux pipelines séparés : un pour les micro-opérations entières, un autre pour les micro-opérations flottantes. On parle alors de '''double émission entière-flottante'''. {|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, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité. La plupart des premiers processeurs superscalaires étaient de ce type. Les exemples les plus notables sont les processeurs POWER 1 et ses dérivés comme le ''RISC Single Chip''. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle. [[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]] La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisations visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate]. ===L'émission parallèle des branchements=== Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur. 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 modifier le banc de registres. Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. [[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]] Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications et les divisions ? Pour étudier ces questions, nous allons prendre le cas d'un processeur capable d’émettre trois opérations en même temps : deux opérations entières, et une flottante, potentiellement d'autres. {|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 |} 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|Emission multiple des opérations entières, implémentation courante]] Pour les branchements, deux solutions sont possibles. Avec la première, il y a émission parallèle des branchements, à savoir qu'il est possible d'émettre en branchement en parallèle de deux-trois instructions entières. Et cela impose la présence d'une unité de branchement séparée, avec son port d'émission. Avec la seconde, les branchements sont traités comme des instructions entières. Soit les branchements sont exécutés dans une ALU entière, soit il y a une unité de branchement reliée à un port d'émission pour les opérations entières. [[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Emission série et parallèle des branchements]] Les accès mémoire sont traités comme des opérations entières un peu spéciales, sur les processeurs superscalaires historiques. En clair, le processeur peut émettre deux opérations entières, ou une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent. Les raisons à cela étaient nombreuses. Et ces raisons sont encore valables sur des petits processeurs destinés à l'embarqué ou l'informatique industrielle. Seuls les processeurs haute performance modernes font différemment. La raison principale est le cout en circuit. Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse. Pas besoin de rajouter une unité de calcul d'adresse séparée. Mais surtout, permettre l'émission parallèle des accès mémoire ferait passer un processeur triple émission à de la quadruple émission, avec tous les couts que ça implique. Il faudrait rajouter des ports de lecture/écriture au banc de registre généraux, pour traiter un accès mémoire en plus du reste. Les circuits d'émission et de chargement devraient être élargit, aussi. Et quitte à payer un tel cout en circuit, il serait plus rentable de l'utiliser pour émettre une quatrième opération entière qu'un accès mémoire. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand-chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2. ===Les CPU superscalaires à plusieurs unités spécialisées=== Le classique deux opérations entières + une flottante était l'idéal pour la triple émission. Même les processeurs modernes à triple émission restent sur cette "norme", car elle est proche de l'optimal. Mais arrivé à la quadruple émission, les possibilités sont devenues plus diverses. L'évolution des CPU superscalaires ont évolués dans des directions différentes, chacun faisant à sa sauce, les choses sont devenues plus confuses. Le seul point commun est qu'ils ont eu tendance à rajouter des unités de calcul. Un point important est que la quadruple émission permet de nombreuses possibilités, mais la plupart ne sont pas intéressantes. Pour donner un exemple concret, prenons un CPU superscalaire très simple, comprenant une ALU entière, une FPU, une unité de branchement, et une unité mémoire. Il est possible de créer un CPU quadruple émission avec ces unités, mais le gain en performance n'en vaudrait pas la peine pour le cout en circuit. Il est rare qu'un bloc de 4 instructions contienne exactement une opération entière, une flottante, un branchement et un accès mémoire. En pratique, les seules combinaisons fréquentes sont les combinaisons calcul + accès mémoire et calcul + branchement. Les combinaisons calcul entier + calcul flottant sont aussi possibles, mais plus rares. Mais on dépasse rarement la double émission. Il est préférable d'utiliser ces unités dans un CPU double ou triple émission, en retirant l'unité de branchement et/ou en couplant l'unité mémoire à l'ALU. En clair, une fois arrivé à la quadruple émission, on ne peut plus se contenter d'une ALU, d'une FPU, d'une unité de branchement et d'une unité mémoire. Les processeurs superscalaires larges n'ont pas le choix que de rajouter des unités de calcul, voire des unités mémoire. Et en pratique, ils ont privilégié l'ajout d'ALU entières, avant de dupliquer les autres circuits. La raison est que les programmes informatiques utilisent beaucoup plus d'additions/soustractions/comparaisons que le reste. Il y a environ 5 opérations entières de base pour une multiplication entière, le ratio est le même quand on remplace la multiplication par un branchement ou un accès mémoire. En conséquence, les ALU entières ont été dupliquées en premier, et dans de nombreux exemplaires. Reste à voir comment a été dupliqué le reste. Les multiplieurs n'ont pas été dupliqués, sauf sur les processeurs superscalaires très larges. La raison est qu'il est rare de devoir exécuter deux multiplications à la fois. C'est la même chose pour le ''barrel shifter''. Deux branchements consécutifs sont quant à eux plus fréquents, c'est même assez courant. Aussi, les unités de branchement ont été dupliquées en second lieu, après les ALU entières. Avoir plusieurs accès mémoire proches est assez courant, surtout sur les CPU de type CISC. Aussi, les unités mémoire ont été dupliquées en troisième lieu. Pour résumer, dupliquer des ALU entières est donc la priorité, suivi par la duplication des unités de branchement, puis celle des accès mémoire, pour terminer par le multiplieur. La FPU est un peu à part. Une autre tendance a été de séparer l'unité mémoire des ALU entières, ce qui a permis l''''émission parallèle des µops mémoire'''. Comme son nom l'indique, l'idée est de séparer les µops mémoire des opérations entières. Il devient alors possible d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière. La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple. La '''double émission flottante''' a été utilisée sur les processeurs Alpha. L'idée est d'émettre deux opérations flottantes à la fois. Sauf qu'une implémentation naïve demanderait de dupliquer la FPU, ce qui aurait un cout en circuit rédhibitoire. Pour rappel, les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant. L'idée est d'avoir des ports d'émission séparés pour l'additionneur flottant et le multiplieur flottant. Le processeur peut alors émettre une addition flottante en même temps qu'une multiplication flottante. Les autres circuits de calcul flottant sont répartis sur ces deux ports d'émission flottants. L'avantage est que cela se marie bien avec l'usage d'une fenêtre d'instruction séparée pour les opérations flottantes. La fenêtre d'instruction a alors deux ports séparés, au lieu d'un seul. Rajouter un second port d'émission flottant n'est pas trop un problème, car le cout lié à l'ajout d'un port n'est pas linéaire. Passer de un port à deux a un cout tolérable, bien plus que de passer de 3 ports à 4 ou de 4 à 5. Il est possible de dupliquer l'additionneur et le multiplieur flottant, ce qui permet d'émettre deux additions et multiplications flottantes en même temps. C'est ce qui est fait sur les processeurs AMD d'architecture Zen 1 et 2. Ils ont deux additionneurs flottants par cœur, deux multiplieurs flottants, chacun avec leur propre port d'émission. Les performances en calcul flottant sont assez impressionnantes pour un processeur de l'époque. [[File:ZEN - émission multiple flottante.png|centre|vignette|upright=2|Microarchitecture Zen 1 d'AMD.]] ==Les ports d'émission et de décodage== Les ports d'émission et de décodage sont des ressources matérielles comme les autres. Dans cette section, nous allons parler de concepts liés aux ports d'émission et de décodage. Nous allons notamment parler du partage des ports d'émission, ainsi que des relations numériques entre ces deux types de ports. ===Le partage des ports d'émission et la ''port contention''=== Il est possible de partager un port d'émission, à savoir de connecter un port d'émission à plusieurs unités de calcul. Nous en avons vu un exemple plus haut, avec l'émission multiple des opérations entières. On peut par exemple connecter un multiplieur et une ALU entière sur le même port d'émission. Pour le moment, nous n'avons vu que des cas où un port d'émission est relié soit à des ALU entières, soit à des FPU, pas aux deux. Mais il existe des processeurs où un même port d'émission alimente à la fois une ALU entière et une FPU. Par exemple, on peut relier un additionneur flottant sur le même port qu'une ALU entière. Il faut noter que cela implique une fenêtre d'instruction centralisée, capable de mettre en attente micro-opérations entières et flottantes. Un exemple est celui des processeurs Core 2 Duo. Ils disposent de 6 ports d'émission, dont 3 ports dédiés à l'unité mémoire. Les 3 ports restants alimentent chacun : une ALU entière, un circuit de calcul flottant et une unité de calcul SSE (une unité de calcul SIMD, qu'on abordera dans quelques chapitres). * Le premier port alimente une ALU entière simple et un multiplieur/diviseur flottant. * Le second alimente une ALU entière, un multiplieur entier et un additionneur flottant. * Le troisième alimente une ALU entière, sans circuit flottant dédié. Dans le même genre, il est possible de partager un port d'émission entre l'unité mémoire et une ALU entière. Cela permet d'utiliser l'ALU entière pour les calculs d'adresse, ce qui évite d'avoir à utiliser une unité de calcul d'adresse distincte. Un exemple est celui du processeur superscalaire double émission Power PC 440. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. L'organisation en question permet soit d'émettre un accès mémoire en même temps qu'une opération entière, soit d'émettre deux opérations entières simples, soit d’émettre une multiplication et une addition/soustraction/comparaison. Une organisation simple, mais très efficace ! [[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]] Regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles, qui ne se présentent que pour certaines combinaisons de micro-opérations bien précises. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port. Le nombre de ports peut être un facteur limitant pour la performance dans certaines situations de '''''port contention''''' où on a assez d'ALU pour exécuter N micro-opérations, mais où la répartition des ALUs sur les ports l’empêche. Mais on peut réduire la ''port contention'' en répartissant correctement les ALU sur les ports d'émission. Le choix en question dépendent fortement du fait qu'en moyenne, que certaines paires d'instructions sont plus fréquentes que d'autres. Tout cela guide ce choix de répartition des ALUs sur les ports. Nous venons de voir que la répartition des ALU sur les ports d'émission est très variable d'un processeur à l'autre. Les divers choix possibles sont tous des compromis entre deux forces : réduire le nombre de ports d'émission d'un côté, garder de bonnes performances en réduisant la ''port contention'' de l'autre. Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction relativement simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ? Par contre, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal réparti les ALU. ===La séparation entre largeur de décodage et d'émission=== Sur les processeurs superscalaires larges, le processeur peut émettre plus d'instructions qu'il peut en décoder. Plus haut, j'ai dit que l'unité d'émission avait parfois plus de ports d'émission que de ports de décodage. La raison est assez simple, et elle s'explique assez bien avec un exemple. Prenons un processeur avec trois ALU entières, une FPU, une unité de branchement et une unité mémoire. Chacune a son propre port d'émission, ce qui fait que le CPU est capable d'émettre 6 µops. Du moins, c'est la théorie, car les conditions pour sont assez rares. Il faut qu'en chargeant un paquet de 6 instructions, il y ait exactement trois instructions entières, une flottante, un branchement et un accès mémoire. Si le paquet contient 5 instructions entières et un branchement, seules quatre instructions sont émises. Dans cet exemple, dans la grande majorité des cas, seuls 4 ports seront utilisés en moyenne et deux seront sous-utilisés. Il s'agit là d'une moyenne, qui dépend du code, et des instructions exactes à exécuter. Cependant, cette moyenne nous donne un moyen d'économiser beaucoup de circuits pour un cout en performance mineur. L'idée est de ne charger que des blocs de 4 instructions, mais d'avoir 6 ports d'émission. Le processeur charge et décode 4 instructions, les quatre instructions sont envoyées sur les ports d'émission adéquat. Le cout en transistors est alors situé entre celui d'un CPU quadruple émission et sextuple émission. Et les performances sont aussi entre les deux. Pour les performances, c'est parce que cela permet d'avoir un CPU quadruple émission avec bien moins de contraintes d’appariement. Imaginez qu'on se soit limité à quatre ports d'émission : il aurait fallu mettre plusieurs unités sur le même port d'émission. En conséquence, il y aurait eu des combinaisons interdites, comme émettre deux instructions entières + un branchement + un accès mémoire ; ou deux instructions entières + une opération flottante + un accès mémoire. De tells appariement interdits sont possibles en utilisant les six ports d'émission. ==L'émission multiple des accès mémoire== Les premiers processeurs superscalaires ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles. Par contre, c'est autre chose sur les processeurs superscalaires larges. Vu qu'ils travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares. L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. La ''load/store queue'' reste une structure unique, mais on peut lui rajouter des ports. Et à ce peit jeu, il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire. La première duplique les unités de calcul, mais pas les ports d'accès au cache. Les accès mémoire sont alors accumulés dans la ''load/store queue'', qui sont ensuite traités un par un par le cache. Un processeur utilisant cette technique est, par exemple, capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. Un exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. Ces processeurs pouvaient calculer trois adresses simultanément, mais ne pouvaient faire qu'un seul accès au cache par cycle. Le cout en circuit est modéré : les unités de calcul coutent très peu, les ports de la ''load/store queue'' coutent déjà un peu plus. La seconde méthode ajoute des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultanées. Il faut alors dupliquer des unités de calcul, rajouter les ports sur le cache, mais aussi rajouter des ports sur la ''load/store queue'', pour qu'elle échange des données/commandes mémoire avec le cache. Le cout en circuits est alors conséquent Quelques processeurs utilisaient des solutions intermédiaires, un exemple classique étant les Pentium 1 et 2 d'Intel. Mais nous en parlerons dans le chapitre suivant. [[File:Processeur non-superscalaire basique 02.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]] Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre. Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Le Pentium 4 était dans ce cas, mais il était un peu seul dans sa catégorie. Un cas similaire est celui où l'unité mémoire est alimentée par une fenêtre de micro-opération mélangeant instructions mémoire et entières. Les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. Dans ce cas, l'émission multiple est rarement implémentée. Le cas suivant a une file pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique. Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui pouvaient seulement émettre une lecture en même temps qu'une écriture. Ils avaient deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse. [[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]] Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture demande d'utiliser une file d'écriture multiport, ce qui est assez compliqué. Implémenter des lectures en plus est plus simple : il suffit de rajouter une seconde file de µops LOAD et une unité de calcul, alimentées via un second port d'émission. Il est aussi possible de rajouter un second port de lecture au cache, mais ce n'est pas obligatoire. Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse. De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports de lecture et d'écriture car il y a plus de lecture que d'écritures. Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. : Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse. [[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]] Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettent plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail. Pour résumer, l'évolution des CPU superscalaires a été la suivante : * pipeline mémorie fusionné avec le pipeline entier ; * séparation du pipeline mémoire du reste, avec émission parallèle des accès mémoire ; * émission multiple, mais sans dupliquer les ports du cache ; * émission multiple, avec émission simultanée d'une lecture et d’une écriture dans le cache ; * émission multiple avec plusieurs accès en lecture au cache. ==Les optimisations des processeurs superscalaires larges== Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul. ===L'étape de chargement d'un CPU superscalaire large=== Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 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 autoaligné.]] Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements ! [[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]] Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez 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 microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total). Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois 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 microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement. Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations. ===La micro-fusion des processeurs Intel et AMD=== La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''. Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''. Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout. La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Le parallélisme mémoire | prevText=Le parallélisme mémoire | next=Exemples de microarchitectures CPU : le cas du x86 | nextText=Exemples de microarchitectures CPU : le cas du x86 }} </noinclude> 2b9lvl3yeey99y36818y5o52ojk7twz 772687 772686 2026-09-21T14:43:23Z Mewtow 31375 /* L'émission multiple des micro-opérations entières */ 772687 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 tout premiers processeurs superscalaires pouvaient émettre deux µops en même temps, ce qui portait le nom de '''double émission'''. Mais le nombre a rapidement augmenté et les processeurs modernes 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. ==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. Par exemple, l'unité de chargement charge deux fois plus d'instructions, mais n'est pas modifiée en profondeur. L'unité d'émission et les circuits d'exécution dans le désordre sont eux aussi altérés. Les unités de calcul sont souvent dupliquées, sauf sur les tout premiers CPU superscalaires. {|class="wikitable" |- ! rowspan="2" | Processeur sans émission multiple | rowspan="2" | Chargement | rowspan="2" | Décodage | rowspan="2" | Renommage | rowspan="2" | Émission | Exécution / ALU | rowspan="2" | ''Commit''/ROB |- | Exécution / ALU |- ! rowspan="4" | Processeur superscalaire | rowspan="4" | Chargement | rowspan="2" | Décodage | rowspan="4" | Renommage | rowspan="4" | Émission | Exécution / ALU | rowspan="4" | ''Commit''/ROB |- | Exécution / ALU |- | rowspan="2" | Décodage | Exécution / ALU |- | Exécution / ALU |} Voyons ce qu'il en est dans le détail. ===Les unités de calcul d'un processeur superscalaire=== Émettre N instructions simultanément demande implicitement d'exécuter N instructions par cycle, voire plus si des instructions multicycles viennent mettre leur grain de sel. Intuitivement, on se dit que les unités de calcul doivent être dupliquées. Et c'est une solution viable pour les processeurs très simples, avec seulement quelques ALU entières. Par exemple, un processeur RISC basique avec seulement une ALU entière et un ''barrel shifter'' peut être rendu superscalaire en dupliquant les deux circuits. Le problème, est que cette solution marche assez mal dès que le processeur contient des circuits multiplieurs et/ou une FPU. Par exemple, un processeur à double émission devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, etc. 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 ! Par contre, l'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission. Un autre cas extrême est celui où aucune unité de calcul n'est dupliquée. Pour comprendre comment c'est possible rappelez-vous qu'un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et éventuellement une unité de branchement. Cela suffit pour créer un processeur quadruple émission, très limité. Un tel processeur peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire. Le problème est qu'il est rare que l'on ait à exécuter en même temps un calcul entier, un calcul flottant, un accès mémoire et un branchement. La quadruple émission n'est donc que rarement utilisée. En pratique, les seules combinaisons fréquentes sont les combinaisons calcul + accès mémoire et calcul + branchement. Les combinaisons calcul entier + calcul flottant sont aussi possibles, mais plus rares. Mais on dépasse rarement la double émission. En clair, à partir de la quadruple émission, on ne peut plus se contenter d'une ALU, d'une FPU, d'une unité de branchement et d'une unité mémoire. Les processeurs superscalaires larges n'ont pas le choix que de rajouter des unités de calcul. Reste à voir ce qui est dupliqué en pratique. 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. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant. La conséquence est que les processeurs superscalaires ont des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a des paires d'instructions autorisées et des paires interdites. Par exemple, l'exécution simultanée de deux branchements est interdite, les branchements sont exécutés l'un après l'autre. Mais il est possible d'émettre un branchement en même temps qu'une autre instruction, en espérant que la prédiction de branchement ait fait une bonne prédiction. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur. En pratique, ces contraintes d’appariement ont peu d’impact. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière, le ratio est le même quand on remplace la multiplication par un branchement ou un accès mémoire. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Deux branchements consécutifs sont quant à eux plus fréquents, idem pour deux accès mémoire. Dupliquer des ALU entières est donc la priorité, suivi par la duplication des unités de branchement, puis celle des accès mémoire, pour terminer par le multiplieur et la FPU. ===Le chargement de plusieurs instructions consécutives=== Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large. Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence. Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas. Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement non-pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc. [[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]] ===Le décodage parallèle des instructions=== Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits. Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur. Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! ===L'unité d'émission d'un processeur superscalaire=== La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. Nous verrons pourquoi dans la suite, mais on peut dire que c'est juste plus simple et plus efficace ainsi. Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop candidate, sous entendu "candidate à l'émission". L'unité d'émission détecte les dépendances entre la µop candidate et les µops en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en cours d'exécution dans le pipeline. mais en plus, elle doit tester les dépendances entre les µops candidates. Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément, il doit détecter les paires d'instructions dépendantes et les sérialiser. Avec une fenêtre d'instruction, il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil gérent les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires modernes utilisent tous une fenêtre d'instruction centralisée ou décentralisée, et non des ''scoreboard''. Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps, là encore en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire peut émettre 8 µops à la fois, on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle. ===L'unité de renommage superscalaire=== Avec la superscalarité, l'unité de renommage de registre doit gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance de registre, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées. Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat. [[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]] Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante. [[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]] Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande. [[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]] ===Le banc de registre d'un processeur superscalaire=== Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement. 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. ==L'évolution historique des processeurs superscalaires== Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs triple émission permettent eux d’émettre trois instructions à la fois. Les tout premiers processeurs superscalaires avaient un budget en transistors très limité, ce qui fait qu'ils ne pouvaient pas être "totalement superscalaires". Par exemple, un processeur double émission ne pouvait pas émettre deux opérations entières en même temps, ni deux opérations flottantes, ni une opération entière avec un accès mémoire. Il y avait des paires d'instructions autorisées et des paires interdites. Les '''contraintes d'appariement''' sur les instructions à émettre en même temps étaient très strictes. L'intérêt de ces contraintes d’appariement est que le processeur était très simple, il n'y avait pas besoin de rajouter des circuits complexes pour rendre le processeur superscalaire. Les CPU superscalaires historiques ne faisaient que mieux utiliser des unités de calcul déjà présentes. Prenons un processeur non-superscalaire simple, illustré ci-dessous. Il contient : une unité de chargement, un décodeur, une unité d'émission, une ALU, une FPU, les bancs de registre entier et flottants, et une unité mémoire. Nous avons placé l'unité mémoire à la suite de l'ALU entière, car ce sera important pour les explications qui vont suivre. [[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]] Les processeurs superscalaires historiques modifiaient assez peu le schéma précédent. L'unité de chargement était adaptée pour lire plusieurs instructions depuis la mémoire, il y avait un seul décodeur, l'unité d'émission était assez peu modifiée. Mais une chose était certaine : aucune unité de calcul n'était dupliquée, le banc de registre n'était presque pas modifié. A la rigueur, gérer la triple 'émission avec les branchements demandait parfois d'ajouter des ports de lecture sur le banc de registre généraux, pas plus. ===La double émission entière-flottante=== Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est d'émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Le processeur a donc deux pipelines séparés : un pour les micro-opérations entières, un autre pour les micro-opérations flottantes. On parle alors de '''double émission entière-flottante'''. {|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, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité. La plupart des premiers processeurs superscalaires étaient de ce type. Les exemples les plus notables sont les processeurs POWER 1 et ses dérivés comme le ''RISC Single Chip''. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle. [[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]] La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisations visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate]. ===L'émission parallèle des branchements=== Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur. 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 modifier le banc de registres. Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. [[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]] Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications et les divisions ? Pour étudier ces questions, nous allons prendre le cas d'un processeur capable d’émettre trois opérations en même temps : deux opérations entières, et une flottante, potentiellement d'autres. {|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 |} 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|Emission multiple des opérations entières, implémentation courante]] Pour les branchements, deux solutions sont possibles. Avec la première, il y a émission parallèle des branchements, à savoir qu'il est possible d'émettre en branchement en parallèle de deux-trois instructions entières. Et cela impose la présence d'une unité de branchement séparée, avec son port d'émission. Avec la seconde, les branchements sont traités comme des instructions entières. Soit les branchements sont exécutés dans une ALU entière, soit il y a une unité de branchement reliée à un port d'émission pour les opérations entières. [[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Emission série et parallèle des branchements]] Les accès mémoire sont traités comme des opérations entières un peu spéciales, sur les processeurs superscalaires historiques. En clair, le processeur peut émettre deux opérations entières, ou une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent. Les raisons à cela étaient nombreuses. Et ces raisons sont encore valables sur des petits processeurs destinés à l'embarqué ou l'informatique industrielle. Seuls les processeurs haute performance modernes font différemment. La raison principale est le cout en circuit. Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse. Pas besoin de rajouter une unité de calcul d'adresse séparée. Mais surtout, permettre l'émission parallèle des accès mémoire ferait passer un processeur triple émission à de la quadruple émission, avec tous les couts que ça implique. Il faudrait rajouter des ports de lecture/écriture au banc de registre généraux, pour traiter un accès mémoire en plus du reste. Les circuits d'émission et de chargement devraient être élargit, aussi. Et quitte à payer un tel cout en circuit, il serait plus rentable de l'utiliser pour émettre une quatrième opération entière qu'un accès mémoire. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand-chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2. {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 |- | * ALU entière * Multiplieur | * ALU entière * Barrel shifter * Unité de branchement * Unité mémoire | FPU |- | * Additions, soustractions, opérations bit à bit, etc. * Multiplication | * Additions, soustractions, opérations bit à bit, etc. * Décalages et rotations * Branchements * Accès mémoire |Opération flottante |} ===Les CPU superscalaires à plusieurs unités spécialisées=== Le classique deux opérations entières + une flottante était l'idéal pour la triple émission. Même les processeurs modernes à triple émission restent sur cette "norme", car elle est proche de l'optimal. Mais arrivé à la quadruple émission, les possibilités sont devenues plus diverses. L'évolution des CPU superscalaires ont évolués dans des directions différentes, chacun faisant à sa sauce, les choses sont devenues plus confuses. Le seul point commun est qu'ils ont eu tendance à rajouter des unités de calcul. Un point important est que la quadruple émission permet de nombreuses possibilités, mais la plupart ne sont pas intéressantes. Pour donner un exemple concret, prenons un CPU superscalaire très simple, comprenant une ALU entière, une FPU, une unité de branchement, et une unité mémoire. Il est possible de créer un CPU quadruple émission avec ces unités, mais le gain en performance n'en vaudrait pas la peine pour le cout en circuit. Il est rare qu'un bloc de 4 instructions contienne exactement une opération entière, une flottante, un branchement et un accès mémoire. En pratique, les seules combinaisons fréquentes sont les combinaisons calcul + accès mémoire et calcul + branchement. Les combinaisons calcul entier + calcul flottant sont aussi possibles, mais plus rares. Mais on dépasse rarement la double émission. Il est préférable d'utiliser ces unités dans un CPU double ou triple émission, en retirant l'unité de branchement et/ou en couplant l'unité mémoire à l'ALU. En clair, une fois arrivé à la quadruple émission, on ne peut plus se contenter d'une ALU, d'une FPU, d'une unité de branchement et d'une unité mémoire. Les processeurs superscalaires larges n'ont pas le choix que de rajouter des unités de calcul, voire des unités mémoire. Et en pratique, ils ont privilégié l'ajout d'ALU entières, avant de dupliquer les autres circuits. La raison est que les programmes informatiques utilisent beaucoup plus d'additions/soustractions/comparaisons que le reste. Il y a environ 5 opérations entières de base pour une multiplication entière, le ratio est le même quand on remplace la multiplication par un branchement ou un accès mémoire. En conséquence, les ALU entières ont été dupliquées en premier, et dans de nombreux exemplaires. Reste à voir comment a été dupliqué le reste. Les multiplieurs n'ont pas été dupliqués, sauf sur les processeurs superscalaires très larges. La raison est qu'il est rare de devoir exécuter deux multiplications à la fois. C'est la même chose pour le ''barrel shifter''. Deux branchements consécutifs sont quant à eux plus fréquents, c'est même assez courant. Aussi, les unités de branchement ont été dupliquées en second lieu, après les ALU entières. Avoir plusieurs accès mémoire proches est assez courant, surtout sur les CPU de type CISC. Aussi, les unités mémoire ont été dupliquées en troisième lieu. Pour résumer, dupliquer des ALU entières est donc la priorité, suivi par la duplication des unités de branchement, puis celle des accès mémoire, pour terminer par le multiplieur. La FPU est un peu à part. Une autre tendance a été de séparer l'unité mémoire des ALU entières, ce qui a permis l''''émission parallèle des µops mémoire'''. Comme son nom l'indique, l'idée est de séparer les µops mémoire des opérations entières. Il devient alors possible d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière. La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple. La '''double émission flottante''' a été utilisée sur les processeurs Alpha. L'idée est d'émettre deux opérations flottantes à la fois. Sauf qu'une implémentation naïve demanderait de dupliquer la FPU, ce qui aurait un cout en circuit rédhibitoire. Pour rappel, les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant. L'idée est d'avoir des ports d'émission séparés pour l'additionneur flottant et le multiplieur flottant. Le processeur peut alors émettre une addition flottante en même temps qu'une multiplication flottante. Les autres circuits de calcul flottant sont répartis sur ces deux ports d'émission flottants. L'avantage est que cela se marie bien avec l'usage d'une fenêtre d'instruction séparée pour les opérations flottantes. La fenêtre d'instruction a alors deux ports séparés, au lieu d'un seul. Rajouter un second port d'émission flottant n'est pas trop un problème, car le cout lié à l'ajout d'un port n'est pas linéaire. Passer de un port à deux a un cout tolérable, bien plus que de passer de 3 ports à 4 ou de 4 à 5. Il est possible de dupliquer l'additionneur et le multiplieur flottant, ce qui permet d'émettre deux additions et multiplications flottantes en même temps. C'est ce qui est fait sur les processeurs AMD d'architecture Zen 1 et 2. Ils ont deux additionneurs flottants par cœur, deux multiplieurs flottants, chacun avec leur propre port d'émission. Les performances en calcul flottant sont assez impressionnantes pour un processeur de l'époque. [[File:ZEN - émission multiple flottante.png|centre|vignette|upright=2|Microarchitecture Zen 1 d'AMD.]] ==Les ports d'émission et de décodage== Les ports d'émission et de décodage sont des ressources matérielles comme les autres. Dans cette section, nous allons parler de concepts liés aux ports d'émission et de décodage. Nous allons notamment parler du partage des ports d'émission, ainsi que des relations numériques entre ces deux types de ports. ===Le partage des ports d'émission et la ''port contention''=== Il est possible de partager un port d'émission, à savoir de connecter un port d'émission à plusieurs unités de calcul. Nous en avons vu un exemple plus haut, avec l'émission multiple des opérations entières. On peut par exemple connecter un multiplieur et une ALU entière sur le même port d'émission. Pour le moment, nous n'avons vu que des cas où un port d'émission est relié soit à des ALU entières, soit à des FPU, pas aux deux. Mais il existe des processeurs où un même port d'émission alimente à la fois une ALU entière et une FPU. Par exemple, on peut relier un additionneur flottant sur le même port qu'une ALU entière. Il faut noter que cela implique une fenêtre d'instruction centralisée, capable de mettre en attente micro-opérations entières et flottantes. Un exemple est celui des processeurs Core 2 Duo. Ils disposent de 6 ports d'émission, dont 3 ports dédiés à l'unité mémoire. Les 3 ports restants alimentent chacun : une ALU entière, un circuit de calcul flottant et une unité de calcul SSE (une unité de calcul SIMD, qu'on abordera dans quelques chapitres). * Le premier port alimente une ALU entière simple et un multiplieur/diviseur flottant. * Le second alimente une ALU entière, un multiplieur entier et un additionneur flottant. * Le troisième alimente une ALU entière, sans circuit flottant dédié. Dans le même genre, il est possible de partager un port d'émission entre l'unité mémoire et une ALU entière. Cela permet d'utiliser l'ALU entière pour les calculs d'adresse, ce qui évite d'avoir à utiliser une unité de calcul d'adresse distincte. Un exemple est celui du processeur superscalaire double émission Power PC 440. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. L'organisation en question permet soit d'émettre un accès mémoire en même temps qu'une opération entière, soit d'émettre deux opérations entières simples, soit d’émettre une multiplication et une addition/soustraction/comparaison. Une organisation simple, mais très efficace ! [[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]] Regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles, qui ne se présentent que pour certaines combinaisons de micro-opérations bien précises. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port. Le nombre de ports peut être un facteur limitant pour la performance dans certaines situations de '''''port contention''''' où on a assez d'ALU pour exécuter N micro-opérations, mais où la répartition des ALUs sur les ports l’empêche. Mais on peut réduire la ''port contention'' en répartissant correctement les ALU sur les ports d'émission. Le choix en question dépendent fortement du fait qu'en moyenne, que certaines paires d'instructions sont plus fréquentes que d'autres. Tout cela guide ce choix de répartition des ALUs sur les ports. Nous venons de voir que la répartition des ALU sur les ports d'émission est très variable d'un processeur à l'autre. Les divers choix possibles sont tous des compromis entre deux forces : réduire le nombre de ports d'émission d'un côté, garder de bonnes performances en réduisant la ''port contention'' de l'autre. Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction relativement simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ? Par contre, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal réparti les ALU. ===La séparation entre largeur de décodage et d'émission=== Sur les processeurs superscalaires larges, le processeur peut émettre plus d'instructions qu'il peut en décoder. Plus haut, j'ai dit que l'unité d'émission avait parfois plus de ports d'émission que de ports de décodage. La raison est assez simple, et elle s'explique assez bien avec un exemple. Prenons un processeur avec trois ALU entières, une FPU, une unité de branchement et une unité mémoire. Chacune a son propre port d'émission, ce qui fait que le CPU est capable d'émettre 6 µops. Du moins, c'est la théorie, car les conditions pour sont assez rares. Il faut qu'en chargeant un paquet de 6 instructions, il y ait exactement trois instructions entières, une flottante, un branchement et un accès mémoire. Si le paquet contient 5 instructions entières et un branchement, seules quatre instructions sont émises. Dans cet exemple, dans la grande majorité des cas, seuls 4 ports seront utilisés en moyenne et deux seront sous-utilisés. Il s'agit là d'une moyenne, qui dépend du code, et des instructions exactes à exécuter. Cependant, cette moyenne nous donne un moyen d'économiser beaucoup de circuits pour un cout en performance mineur. L'idée est de ne charger que des blocs de 4 instructions, mais d'avoir 6 ports d'émission. Le processeur charge et décode 4 instructions, les quatre instructions sont envoyées sur les ports d'émission adéquat. Le cout en transistors est alors situé entre celui d'un CPU quadruple émission et sextuple émission. Et les performances sont aussi entre les deux. Pour les performances, c'est parce que cela permet d'avoir un CPU quadruple émission avec bien moins de contraintes d’appariement. Imaginez qu'on se soit limité à quatre ports d'émission : il aurait fallu mettre plusieurs unités sur le même port d'émission. En conséquence, il y aurait eu des combinaisons interdites, comme émettre deux instructions entières + un branchement + un accès mémoire ; ou deux instructions entières + une opération flottante + un accès mémoire. De tells appariement interdits sont possibles en utilisant les six ports d'émission. ==L'émission multiple des accès mémoire== Les premiers processeurs superscalaires ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles. Par contre, c'est autre chose sur les processeurs superscalaires larges. Vu qu'ils travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares. L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. La ''load/store queue'' reste une structure unique, mais on peut lui rajouter des ports. Et à ce peit jeu, il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire. La première duplique les unités de calcul, mais pas les ports d'accès au cache. Les accès mémoire sont alors accumulés dans la ''load/store queue'', qui sont ensuite traités un par un par le cache. Un processeur utilisant cette technique est, par exemple, capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. Un exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. Ces processeurs pouvaient calculer trois adresses simultanément, mais ne pouvaient faire qu'un seul accès au cache par cycle. Le cout en circuit est modéré : les unités de calcul coutent très peu, les ports de la ''load/store queue'' coutent déjà un peu plus. La seconde méthode ajoute des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultanées. Il faut alors dupliquer des unités de calcul, rajouter les ports sur le cache, mais aussi rajouter des ports sur la ''load/store queue'', pour qu'elle échange des données/commandes mémoire avec le cache. Le cout en circuits est alors conséquent Quelques processeurs utilisaient des solutions intermédiaires, un exemple classique étant les Pentium 1 et 2 d'Intel. Mais nous en parlerons dans le chapitre suivant. [[File:Processeur non-superscalaire basique 02.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]] Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre. Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Le Pentium 4 était dans ce cas, mais il était un peu seul dans sa catégorie. Un cas similaire est celui où l'unité mémoire est alimentée par une fenêtre de micro-opération mélangeant instructions mémoire et entières. Les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. Dans ce cas, l'émission multiple est rarement implémentée. Le cas suivant a une file pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique. Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui pouvaient seulement émettre une lecture en même temps qu'une écriture. Ils avaient deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse. [[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]] Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture demande d'utiliser une file d'écriture multiport, ce qui est assez compliqué. Implémenter des lectures en plus est plus simple : il suffit de rajouter une seconde file de µops LOAD et une unité de calcul, alimentées via un second port d'émission. Il est aussi possible de rajouter un second port de lecture au cache, mais ce n'est pas obligatoire. Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse. De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports de lecture et d'écriture car il y a plus de lecture que d'écritures. Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. : Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse. [[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]] Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettent plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail. Pour résumer, l'évolution des CPU superscalaires a été la suivante : * pipeline mémorie fusionné avec le pipeline entier ; * séparation du pipeline mémoire du reste, avec émission parallèle des accès mémoire ; * émission multiple, mais sans dupliquer les ports du cache ; * émission multiple, avec émission simultanée d'une lecture et d’une écriture dans le cache ; * émission multiple avec plusieurs accès en lecture au cache. ==Les optimisations des processeurs superscalaires larges== Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul. ===L'étape de chargement d'un CPU superscalaire large=== Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 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 autoaligné.]] Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements ! [[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]] Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez 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 microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total). Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois 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 microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement. Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations. ===La micro-fusion des processeurs Intel et AMD=== La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''. Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''. Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout. La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Le parallélisme mémoire | prevText=Le parallélisme mémoire | next=Exemples de microarchitectures CPU : le cas du x86 | nextText=Exemples de microarchitectures CPU : le cas du x86 }} </noinclude> fb5r6aibzmyacfbmrnsd7gm6s2kt8x0 772688 772687 2026-09-21T14:48:05Z Mewtow 31375 /* L'émission multiple des micro-opérations entières */ 772688 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 tout premiers processeurs superscalaires pouvaient émettre deux µops en même temps, ce qui portait le nom de '''double émission'''. Mais le nombre a rapidement augmenté et les processeurs modernes 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. ==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. Par exemple, l'unité de chargement charge deux fois plus d'instructions, mais n'est pas modifiée en profondeur. L'unité d'émission et les circuits d'exécution dans le désordre sont eux aussi altérés. Les unités de calcul sont souvent dupliquées, sauf sur les tout premiers CPU superscalaires. {|class="wikitable" |- ! rowspan="2" | Processeur sans émission multiple | rowspan="2" | Chargement | rowspan="2" | Décodage | rowspan="2" | Renommage | rowspan="2" | Émission | Exécution / ALU | rowspan="2" | ''Commit''/ROB |- | Exécution / ALU |- ! rowspan="4" | Processeur superscalaire | rowspan="4" | Chargement | rowspan="2" | Décodage | rowspan="4" | Renommage | rowspan="4" | Émission | Exécution / ALU | rowspan="4" | ''Commit''/ROB |- | Exécution / ALU |- | rowspan="2" | Décodage | Exécution / ALU |- | Exécution / ALU |} Voyons ce qu'il en est dans le détail. ===Les unités de calcul d'un processeur superscalaire=== Émettre N instructions simultanément demande implicitement d'exécuter N instructions par cycle, voire plus si des instructions multicycles viennent mettre leur grain de sel. Intuitivement, on se dit que les unités de calcul doivent être dupliquées. Et c'est une solution viable pour les processeurs très simples, avec seulement quelques ALU entières. Par exemple, un processeur RISC basique avec seulement une ALU entière et un ''barrel shifter'' peut être rendu superscalaire en dupliquant les deux circuits. Le problème, est que cette solution marche assez mal dès que le processeur contient des circuits multiplieurs et/ou une FPU. Par exemple, un processeur à double émission devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, etc. 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 ! Par contre, l'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission. Un autre cas extrême est celui où aucune unité de calcul n'est dupliquée. Pour comprendre comment c'est possible rappelez-vous qu'un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et éventuellement une unité de branchement. Cela suffit pour créer un processeur quadruple émission, très limité. Un tel processeur peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire. Le problème est qu'il est rare que l'on ait à exécuter en même temps un calcul entier, un calcul flottant, un accès mémoire et un branchement. La quadruple émission n'est donc que rarement utilisée. En pratique, les seules combinaisons fréquentes sont les combinaisons calcul + accès mémoire et calcul + branchement. Les combinaisons calcul entier + calcul flottant sont aussi possibles, mais plus rares. Mais on dépasse rarement la double émission. En clair, à partir de la quadruple émission, on ne peut plus se contenter d'une ALU, d'une FPU, d'une unité de branchement et d'une unité mémoire. Les processeurs superscalaires larges n'ont pas le choix que de rajouter des unités de calcul. Reste à voir ce qui est dupliqué en pratique. 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. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant. La conséquence est que les processeurs superscalaires ont des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a des paires d'instructions autorisées et des paires interdites. Par exemple, l'exécution simultanée de deux branchements est interdite, les branchements sont exécutés l'un après l'autre. Mais il est possible d'émettre un branchement en même temps qu'une autre instruction, en espérant que la prédiction de branchement ait fait une bonne prédiction. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur. En pratique, ces contraintes d’appariement ont peu d’impact. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière, le ratio est le même quand on remplace la multiplication par un branchement ou un accès mémoire. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Deux branchements consécutifs sont quant à eux plus fréquents, idem pour deux accès mémoire. Dupliquer des ALU entières est donc la priorité, suivi par la duplication des unités de branchement, puis celle des accès mémoire, pour terminer par le multiplieur et la FPU. ===Le chargement de plusieurs instructions consécutives=== Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large. Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence. Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas. Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement non-pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc. [[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]] ===Le décodage parallèle des instructions=== Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits. Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur. Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! ===L'unité d'émission d'un processeur superscalaire=== La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. Nous verrons pourquoi dans la suite, mais on peut dire que c'est juste plus simple et plus efficace ainsi. Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop candidate, sous entendu "candidate à l'émission". L'unité d'émission détecte les dépendances entre la µop candidate et les µops en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en cours d'exécution dans le pipeline. mais en plus, elle doit tester les dépendances entre les µops candidates. Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément, il doit détecter les paires d'instructions dépendantes et les sérialiser. Avec une fenêtre d'instruction, il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil gérent les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires modernes utilisent tous une fenêtre d'instruction centralisée ou décentralisée, et non des ''scoreboard''. Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps, là encore en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire peut émettre 8 µops à la fois, on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle. ===L'unité de renommage superscalaire=== Avec la superscalarité, l'unité de renommage de registre doit gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance de registre, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées. Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat. [[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]] Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante. [[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]] Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande. [[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]] ===Le banc de registre d'un processeur superscalaire=== Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement. 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. ==L'évolution historique des processeurs superscalaires== Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs triple émission permettent eux d’émettre trois instructions à la fois. Les tout premiers processeurs superscalaires avaient un budget en transistors très limité, ce qui fait qu'ils ne pouvaient pas être "totalement superscalaires". Par exemple, un processeur double émission ne pouvait pas émettre deux opérations entières en même temps, ni deux opérations flottantes, ni une opération entière avec un accès mémoire. Il y avait des paires d'instructions autorisées et des paires interdites. Les '''contraintes d'appariement''' sur les instructions à émettre en même temps étaient très strictes. L'intérêt de ces contraintes d’appariement est que le processeur était très simple, il n'y avait pas besoin de rajouter des circuits complexes pour rendre le processeur superscalaire. Les CPU superscalaires historiques ne faisaient que mieux utiliser des unités de calcul déjà présentes. Prenons un processeur non-superscalaire simple, illustré ci-dessous. Il contient : une unité de chargement, un décodeur, une unité d'émission, une ALU, une FPU, les bancs de registre entier et flottants, et une unité mémoire. Nous avons placé l'unité mémoire à la suite de l'ALU entière, car ce sera important pour les explications qui vont suivre. [[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]] Les processeurs superscalaires historiques modifiaient assez peu le schéma précédent. L'unité de chargement était adaptée pour lire plusieurs instructions depuis la mémoire, il y avait un seul décodeur, l'unité d'émission était assez peu modifiée. Mais une chose était certaine : aucune unité de calcul n'était dupliquée, le banc de registre n'était presque pas modifié. A la rigueur, gérer la triple 'émission avec les branchements demandait parfois d'ajouter des ports de lecture sur le banc de registre généraux, pas plus. ===La double émission entière-flottante=== Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est d'émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Le processeur a donc deux pipelines séparés : un pour les micro-opérations entières, un autre pour les micro-opérations flottantes. On parle alors de '''double émission entière-flottante'''. {|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, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité. La plupart des premiers processeurs superscalaires étaient de ce type. Les exemples les plus notables sont les processeurs POWER 1 et ses dérivés comme le ''RISC Single Chip''. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle. [[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]] La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisations visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate]. ===L'émission parallèle des branchements=== Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur. 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 modifier le banc de registres. Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. [[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]] Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications et les divisions ? Pour étudier ces questions, nous allons prendre le cas d'un processeur capable d’émettre trois opérations en même temps : deux opérations entières, et une flottante, potentiellement d'autres. {|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 |} 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|Emission multiple des opérations entières, implémentation courante]] Pour les branchements, deux solutions sont possibles. Avec la première, il y a émission parallèle des branchements, à savoir qu'il est possible d'émettre en branchement en parallèle de deux-trois instructions entières. Et cela impose la présence d'une unité de branchement séparée, avec son port d'émission. Avec la seconde, les branchements sont traités comme des instructions entières. Soit les branchements sont exécutés dans une ALU entière, soit il y a une unité de branchement reliée à un port d'émission pour les opérations entières. [[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Emission série et parallèle des branchements]] Avec la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales, sur les processeurs superscalaires historiques. En clair, le processeur peut émettre 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. De plus, 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. Ce compromis change cependant quand on passe à la quadruple émission. {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 |- | * ALU entière * Multiplieur | * ALU entière * Barrel shifter * Unité de branchement * Unité mémoire | FPU |- | * Additions, soustractions, opérations bit à bit, etc. * Multiplication | * Additions, soustractions, opérations bit à bit, etc. * Décalages et rotations * Branchements * Accès mémoire |Opération flottante |} ===Les CPU superscalaires à plusieurs unités spécialisées=== Le classique deux opérations entières + une flottante était l'idéal pour la triple émission. Même les processeurs modernes à triple émission restent sur cette "norme", car elle est proche de l'optimal. Mais arrivé à la quadruple émission, les possibilités sont devenues plus diverses. L'évolution des CPU superscalaires ont évolués dans des directions différentes, chacun faisant à sa sauce, les choses sont devenues plus confuses. Le seul point commun est qu'ils ont eu tendance à rajouter des unités de calcul. Un point important est que la quadruple émission permet de nombreuses possibilités, mais la plupart ne sont pas intéressantes. Pour donner un exemple concret, prenons un CPU superscalaire très simple, comprenant une ALU entière, une FPU, une unité de branchement, et une unité mémoire. Il est possible de créer un CPU quadruple émission avec ces unités, mais le gain en performance n'en vaudrait pas la peine pour le cout en circuit. Il est rare qu'un bloc de 4 instructions contienne exactement une opération entière, une flottante, un branchement et un accès mémoire. En pratique, les seules combinaisons fréquentes sont les combinaisons calcul + accès mémoire et calcul + branchement. Les combinaisons calcul entier + calcul flottant sont aussi possibles, mais plus rares. Mais on dépasse rarement la double émission. Il est préférable d'utiliser ces unités dans un CPU double ou triple émission, en retirant l'unité de branchement et/ou en couplant l'unité mémoire à l'ALU. En clair, une fois arrivé à la quadruple émission, on ne peut plus se contenter d'une ALU, d'une FPU, d'une unité de branchement et d'une unité mémoire. Les processeurs superscalaires larges n'ont pas le choix que de rajouter des unités de calcul, voire des unités mémoire. Et en pratique, ils ont privilégié l'ajout d'ALU entières, avant de dupliquer les autres circuits. La raison est que les programmes informatiques utilisent beaucoup plus d'additions/soustractions/comparaisons que le reste. Il y a environ 5 opérations entières de base pour une multiplication entière, le ratio est le même quand on remplace la multiplication par un branchement ou un accès mémoire. En conséquence, les ALU entières ont été dupliquées en premier, et dans de nombreux exemplaires. Reste à voir comment a été dupliqué le reste. Les multiplieurs n'ont pas été dupliqués, sauf sur les processeurs superscalaires très larges. La raison est qu'il est rare de devoir exécuter deux multiplications à la fois. C'est la même chose pour le ''barrel shifter''. Deux branchements consécutifs sont quant à eux plus fréquents, c'est même assez courant. Aussi, les unités de branchement ont été dupliquées en second lieu, après les ALU entières. Avoir plusieurs accès mémoire proches est assez courant, surtout sur les CPU de type CISC. Aussi, les unités mémoire ont été dupliquées en troisième lieu. Pour résumer, dupliquer des ALU entières est donc la priorité, suivi par la duplication des unités de branchement, puis celle des accès mémoire, pour terminer par le multiplieur. La FPU est un peu à part. Une autre tendance a été de séparer l'unité mémoire des ALU entières, ce qui a permis l''''émission parallèle des µops mémoire'''. Comme son nom l'indique, l'idée est de séparer les µops mémoire des opérations entières. Il devient alors possible d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière. La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple. La '''double émission flottante''' a été utilisée sur les processeurs Alpha. L'idée est d'émettre deux opérations flottantes à la fois. Sauf qu'une implémentation naïve demanderait de dupliquer la FPU, ce qui aurait un cout en circuit rédhibitoire. Pour rappel, les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant. L'idée est d'avoir des ports d'émission séparés pour l'additionneur flottant et le multiplieur flottant. Le processeur peut alors émettre une addition flottante en même temps qu'une multiplication flottante. Les autres circuits de calcul flottant sont répartis sur ces deux ports d'émission flottants. L'avantage est que cela se marie bien avec l'usage d'une fenêtre d'instruction séparée pour les opérations flottantes. La fenêtre d'instruction a alors deux ports séparés, au lieu d'un seul. Rajouter un second port d'émission flottant n'est pas trop un problème, car le cout lié à l'ajout d'un port n'est pas linéaire. Passer de un port à deux a un cout tolérable, bien plus que de passer de 3 ports à 4 ou de 4 à 5. Il est possible de dupliquer l'additionneur et le multiplieur flottant, ce qui permet d'émettre deux additions et multiplications flottantes en même temps. C'est ce qui est fait sur les processeurs AMD d'architecture Zen 1 et 2. Ils ont deux additionneurs flottants par cœur, deux multiplieurs flottants, chacun avec leur propre port d'émission. Les performances en calcul flottant sont assez impressionnantes pour un processeur de l'époque. [[File:ZEN - émission multiple flottante.png|centre|vignette|upright=2|Microarchitecture Zen 1 d'AMD.]] ==Les ports d'émission et de décodage== Les ports d'émission et de décodage sont des ressources matérielles comme les autres. Dans cette section, nous allons parler de concepts liés aux ports d'émission et de décodage. Nous allons notamment parler du partage des ports d'émission, ainsi que des relations numériques entre ces deux types de ports. ===Le partage des ports d'émission et la ''port contention''=== Il est possible de partager un port d'émission, à savoir de connecter un port d'émission à plusieurs unités de calcul. Nous en avons vu un exemple plus haut, avec l'émission multiple des opérations entières. On peut par exemple connecter un multiplieur et une ALU entière sur le même port d'émission. Pour le moment, nous n'avons vu que des cas où un port d'émission est relié soit à des ALU entières, soit à des FPU, pas aux deux. Mais il existe des processeurs où un même port d'émission alimente à la fois une ALU entière et une FPU. Par exemple, on peut relier un additionneur flottant sur le même port qu'une ALU entière. Il faut noter que cela implique une fenêtre d'instruction centralisée, capable de mettre en attente micro-opérations entières et flottantes. Un exemple est celui des processeurs Core 2 Duo. Ils disposent de 6 ports d'émission, dont 3 ports dédiés à l'unité mémoire. Les 3 ports restants alimentent chacun : une ALU entière, un circuit de calcul flottant et une unité de calcul SSE (une unité de calcul SIMD, qu'on abordera dans quelques chapitres). * Le premier port alimente une ALU entière simple et un multiplieur/diviseur flottant. * Le second alimente une ALU entière, un multiplieur entier et un additionneur flottant. * Le troisième alimente une ALU entière, sans circuit flottant dédié. Dans le même genre, il est possible de partager un port d'émission entre l'unité mémoire et une ALU entière. Cela permet d'utiliser l'ALU entière pour les calculs d'adresse, ce qui évite d'avoir à utiliser une unité de calcul d'adresse distincte. Un exemple est celui du processeur superscalaire double émission Power PC 440. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. L'organisation en question permet soit d'émettre un accès mémoire en même temps qu'une opération entière, soit d'émettre deux opérations entières simples, soit d’émettre une multiplication et une addition/soustraction/comparaison. Une organisation simple, mais très efficace ! [[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]] Regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles, qui ne se présentent que pour certaines combinaisons de micro-opérations bien précises. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port. Le nombre de ports peut être un facteur limitant pour la performance dans certaines situations de '''''port contention''''' où on a assez d'ALU pour exécuter N micro-opérations, mais où la répartition des ALUs sur les ports l’empêche. Mais on peut réduire la ''port contention'' en répartissant correctement les ALU sur les ports d'émission. Le choix en question dépendent fortement du fait qu'en moyenne, que certaines paires d'instructions sont plus fréquentes que d'autres. Tout cela guide ce choix de répartition des ALUs sur les ports. Nous venons de voir que la répartition des ALU sur les ports d'émission est très variable d'un processeur à l'autre. Les divers choix possibles sont tous des compromis entre deux forces : réduire le nombre de ports d'émission d'un côté, garder de bonnes performances en réduisant la ''port contention'' de l'autre. Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction relativement simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ? Par contre, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal réparti les ALU. ===La séparation entre largeur de décodage et d'émission=== Sur les processeurs superscalaires larges, le processeur peut émettre plus d'instructions qu'il peut en décoder. Plus haut, j'ai dit que l'unité d'émission avait parfois plus de ports d'émission que de ports de décodage. La raison est assez simple, et elle s'explique assez bien avec un exemple. Prenons un processeur avec trois ALU entières, une FPU, une unité de branchement et une unité mémoire. Chacune a son propre port d'émission, ce qui fait que le CPU est capable d'émettre 6 µops. Du moins, c'est la théorie, car les conditions pour sont assez rares. Il faut qu'en chargeant un paquet de 6 instructions, il y ait exactement trois instructions entières, une flottante, un branchement et un accès mémoire. Si le paquet contient 5 instructions entières et un branchement, seules quatre instructions sont émises. Dans cet exemple, dans la grande majorité des cas, seuls 4 ports seront utilisés en moyenne et deux seront sous-utilisés. Il s'agit là d'une moyenne, qui dépend du code, et des instructions exactes à exécuter. Cependant, cette moyenne nous donne un moyen d'économiser beaucoup de circuits pour un cout en performance mineur. L'idée est de ne charger que des blocs de 4 instructions, mais d'avoir 6 ports d'émission. Le processeur charge et décode 4 instructions, les quatre instructions sont envoyées sur les ports d'émission adéquat. Le cout en transistors est alors situé entre celui d'un CPU quadruple émission et sextuple émission. Et les performances sont aussi entre les deux. Pour les performances, c'est parce que cela permet d'avoir un CPU quadruple émission avec bien moins de contraintes d’appariement. Imaginez qu'on se soit limité à quatre ports d'émission : il aurait fallu mettre plusieurs unités sur le même port d'émission. En conséquence, il y aurait eu des combinaisons interdites, comme émettre deux instructions entières + un branchement + un accès mémoire ; ou deux instructions entières + une opération flottante + un accès mémoire. De tells appariement interdits sont possibles en utilisant les six ports d'émission. ==L'émission multiple des accès mémoire== Les premiers processeurs superscalaires ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles. Par contre, c'est autre chose sur les processeurs superscalaires larges. Vu qu'ils travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares. L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. La ''load/store queue'' reste une structure unique, mais on peut lui rajouter des ports. Et à ce peit jeu, il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire. La première duplique les unités de calcul, mais pas les ports d'accès au cache. Les accès mémoire sont alors accumulés dans la ''load/store queue'', qui sont ensuite traités un par un par le cache. Un processeur utilisant cette technique est, par exemple, capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. Un exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. Ces processeurs pouvaient calculer trois adresses simultanément, mais ne pouvaient faire qu'un seul accès au cache par cycle. Le cout en circuit est modéré : les unités de calcul coutent très peu, les ports de la ''load/store queue'' coutent déjà un peu plus. La seconde méthode ajoute des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultanées. Il faut alors dupliquer des unités de calcul, rajouter les ports sur le cache, mais aussi rajouter des ports sur la ''load/store queue'', pour qu'elle échange des données/commandes mémoire avec le cache. Le cout en circuits est alors conséquent Quelques processeurs utilisaient des solutions intermédiaires, un exemple classique étant les Pentium 1 et 2 d'Intel. Mais nous en parlerons dans le chapitre suivant. [[File:Processeur non-superscalaire basique 02.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]] Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre. Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Le Pentium 4 était dans ce cas, mais il était un peu seul dans sa catégorie. Un cas similaire est celui où l'unité mémoire est alimentée par une fenêtre de micro-opération mélangeant instructions mémoire et entières. Les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. Dans ce cas, l'émission multiple est rarement implémentée. Le cas suivant a une file pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique. Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui pouvaient seulement émettre une lecture en même temps qu'une écriture. Ils avaient deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse. [[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]] Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture demande d'utiliser une file d'écriture multiport, ce qui est assez compliqué. Implémenter des lectures en plus est plus simple : il suffit de rajouter une seconde file de µops LOAD et une unité de calcul, alimentées via un second port d'émission. Il est aussi possible de rajouter un second port de lecture au cache, mais ce n'est pas obligatoire. Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse. De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports de lecture et d'écriture car il y a plus de lecture que d'écritures. Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. : Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse. [[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]] Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettent plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail. Pour résumer, l'évolution des CPU superscalaires a été la suivante : * pipeline mémorie fusionné avec le pipeline entier ; * séparation du pipeline mémoire du reste, avec émission parallèle des accès mémoire ; * émission multiple, mais sans dupliquer les ports du cache ; * émission multiple, avec émission simultanée d'une lecture et d’une écriture dans le cache ; * émission multiple avec plusieurs accès en lecture au cache. ==Les optimisations des processeurs superscalaires larges== Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul. ===L'étape de chargement d'un CPU superscalaire large=== Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 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 autoaligné.]] Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements ! [[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]] Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez 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 microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total). Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois 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 microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement. Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations. ===La micro-fusion des processeurs Intel et AMD=== La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''. Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''. Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout. La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Le parallélisme mémoire | prevText=Le parallélisme mémoire | next=Exemples de microarchitectures CPU : le cas du x86 | nextText=Exemples de microarchitectures CPU : le cas du x86 }} </noinclude> 062ytxvjehi0z8p76nnpcne4n6l1hl7 772689 772688 2026-09-21T14:48:36Z Mewtow 31375 /* L'émission multiple des micro-opérations entières */ 772689 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 tout premiers processeurs superscalaires pouvaient émettre deux µops en même temps, ce qui portait le nom de '''double émission'''. Mais le nombre a rapidement augmenté et les processeurs modernes 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. ==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. Par exemple, l'unité de chargement charge deux fois plus d'instructions, mais n'est pas modifiée en profondeur. L'unité d'émission et les circuits d'exécution dans le désordre sont eux aussi altérés. Les unités de calcul sont souvent dupliquées, sauf sur les tout premiers CPU superscalaires. {|class="wikitable" |- ! rowspan="2" | Processeur sans émission multiple | rowspan="2" | Chargement | rowspan="2" | Décodage | rowspan="2" | Renommage | rowspan="2" | Émission | Exécution / ALU | rowspan="2" | ''Commit''/ROB |- | Exécution / ALU |- ! rowspan="4" | Processeur superscalaire | rowspan="4" | Chargement | rowspan="2" | Décodage | rowspan="4" | Renommage | rowspan="4" | Émission | Exécution / ALU | rowspan="4" | ''Commit''/ROB |- | Exécution / ALU |- | rowspan="2" | Décodage | Exécution / ALU |- | Exécution / ALU |} Voyons ce qu'il en est dans le détail. ===Les unités de calcul d'un processeur superscalaire=== Émettre N instructions simultanément demande implicitement d'exécuter N instructions par cycle, voire plus si des instructions multicycles viennent mettre leur grain de sel. Intuitivement, on se dit que les unités de calcul doivent être dupliquées. Et c'est une solution viable pour les processeurs très simples, avec seulement quelques ALU entières. Par exemple, un processeur RISC basique avec seulement une ALU entière et un ''barrel shifter'' peut être rendu superscalaire en dupliquant les deux circuits. Le problème, est que cette solution marche assez mal dès que le processeur contient des circuits multiplieurs et/ou une FPU. Par exemple, un processeur à double émission devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, etc. 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 ! Par contre, l'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission. Un autre cas extrême est celui où aucune unité de calcul n'est dupliquée. Pour comprendre comment c'est possible rappelez-vous qu'un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et éventuellement une unité de branchement. Cela suffit pour créer un processeur quadruple émission, très limité. Un tel processeur peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire. Le problème est qu'il est rare que l'on ait à exécuter en même temps un calcul entier, un calcul flottant, un accès mémoire et un branchement. La quadruple émission n'est donc que rarement utilisée. En pratique, les seules combinaisons fréquentes sont les combinaisons calcul + accès mémoire et calcul + branchement. Les combinaisons calcul entier + calcul flottant sont aussi possibles, mais plus rares. Mais on dépasse rarement la double émission. En clair, à partir de la quadruple émission, on ne peut plus se contenter d'une ALU, d'une FPU, d'une unité de branchement et d'une unité mémoire. Les processeurs superscalaires larges n'ont pas le choix que de rajouter des unités de calcul. Reste à voir ce qui est dupliqué en pratique. 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. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant. La conséquence est que les processeurs superscalaires ont des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a des paires d'instructions autorisées et des paires interdites. Par exemple, l'exécution simultanée de deux branchements est interdite, les branchements sont exécutés l'un après l'autre. Mais il est possible d'émettre un branchement en même temps qu'une autre instruction, en espérant que la prédiction de branchement ait fait une bonne prédiction. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur. En pratique, ces contraintes d’appariement ont peu d’impact. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière, le ratio est le même quand on remplace la multiplication par un branchement ou un accès mémoire. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Deux branchements consécutifs sont quant à eux plus fréquents, idem pour deux accès mémoire. Dupliquer des ALU entières est donc la priorité, suivi par la duplication des unités de branchement, puis celle des accès mémoire, pour terminer par le multiplieur et la FPU. ===Le chargement de plusieurs instructions consécutives=== Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large. Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence. Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas. Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement non-pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc. [[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]] ===Le décodage parallèle des instructions=== Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits. Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur. Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! ===L'unité d'émission d'un processeur superscalaire=== La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. Nous verrons pourquoi dans la suite, mais on peut dire que c'est juste plus simple et plus efficace ainsi. Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop candidate, sous entendu "candidate à l'émission". L'unité d'émission détecte les dépendances entre la µop candidate et les µops en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en cours d'exécution dans le pipeline. mais en plus, elle doit tester les dépendances entre les µops candidates. Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément, il doit détecter les paires d'instructions dépendantes et les sérialiser. Avec une fenêtre d'instruction, il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil gérent les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires modernes utilisent tous une fenêtre d'instruction centralisée ou décentralisée, et non des ''scoreboard''. Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps, là encore en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire peut émettre 8 µops à la fois, on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle. ===L'unité de renommage superscalaire=== Avec la superscalarité, l'unité de renommage de registre doit gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance de registre, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées. Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat. [[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]] Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante. [[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]] Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande. [[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]] ===Le banc de registre d'un processeur superscalaire=== Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement. 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. ==L'évolution historique des processeurs superscalaires== Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs triple émission permettent eux d’émettre trois instructions à la fois. Les tout premiers processeurs superscalaires avaient un budget en transistors très limité, ce qui fait qu'ils ne pouvaient pas être "totalement superscalaires". Par exemple, un processeur double émission ne pouvait pas émettre deux opérations entières en même temps, ni deux opérations flottantes, ni une opération entière avec un accès mémoire. Il y avait des paires d'instructions autorisées et des paires interdites. Les '''contraintes d'appariement''' sur les instructions à émettre en même temps étaient très strictes. L'intérêt de ces contraintes d’appariement est que le processeur était très simple, il n'y avait pas besoin de rajouter des circuits complexes pour rendre le processeur superscalaire. Les CPU superscalaires historiques ne faisaient que mieux utiliser des unités de calcul déjà présentes. Prenons un processeur non-superscalaire simple, illustré ci-dessous. Il contient : une unité de chargement, un décodeur, une unité d'émission, une ALU, une FPU, les bancs de registre entier et flottants, et une unité mémoire. Nous avons placé l'unité mémoire à la suite de l'ALU entière, car ce sera important pour les explications qui vont suivre. [[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]] Les processeurs superscalaires historiques modifiaient assez peu le schéma précédent. L'unité de chargement était adaptée pour lire plusieurs instructions depuis la mémoire, il y avait un seul décodeur, l'unité d'émission était assez peu modifiée. Mais une chose était certaine : aucune unité de calcul n'était dupliquée, le banc de registre n'était presque pas modifié. A la rigueur, gérer la triple 'émission avec les branchements demandait parfois d'ajouter des ports de lecture sur le banc de registre généraux, pas plus. ===La double émission entière-flottante=== Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est d'émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Le processeur a donc deux pipelines séparés : un pour les micro-opérations entières, un autre pour les micro-opérations flottantes. On parle alors de '''double émission entière-flottante'''. {|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, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité. La plupart des premiers processeurs superscalaires étaient de ce type. Les exemples les plus notables sont les processeurs POWER 1 et ses dérivés comme le ''RISC Single Chip''. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle. [[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]] La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisations visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate]. ===L'émission parallèle des branchements=== Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur. 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 modifier le banc de registres. Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. [[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]] Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications et les divisions ? Pour étudier ces questions, nous allons prendre le cas d'un processeur capable d’émettre trois opérations en même temps : deux opérations entières, et une flottante, potentiellement d'autres. {|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 |} 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|Emission multiple des opérations entières, implémentation courante]] Pour les branchements, deux solutions sont possibles. Avec la première, il y a émission parallèle des branchements, à savoir qu'il est possible d'émettre en branchement en parallèle de deux-trois instructions entières. Et cela impose la présence d'une unité de branchement séparée, avec son port d'émission. Avec la seconde, les branchements sont traités comme des instructions entières. Soit les branchements sont exécutés dans une ALU entière, soit il y a une unité de branchement reliée à un port d'émission pour les opérations entières. [[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Emission série et parallèle des branchements]] Avec la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales, sur les processeurs superscalaires historiques. En clair, le processeur peut émettre 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. De plus, 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. Ce compromis change cependant quand on passe à la quadruple émission. {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 |- | * ALU entière * Multiplieur | * ALU entière * Barrel shifter * Unité de branchement * Unité mémoire | FPU |- | * Additions, soustractions, opérations bit à bit, etc. * Multiplication | * Additions, soustractions, opérations bit à bit, etc. * Décalages et rotations * Branchements * Accès mémoire |Opération flottante |} ===Les CPU superscalaires à plusieurs unités spécialisées=== Le classique deux opérations entières + une flottante était l'idéal pour la triple émission. Même les processeurs modernes à triple émission restent sur cette "norme", car elle est proche de l'optimal. Mais arrivé à la quadruple émission, les possibilités sont devenues plus diverses. L'évolution des CPU superscalaires ont évolués dans des directions différentes, chacun faisant à sa sauce, les choses sont devenues plus confuses. Le seul point commun est qu'ils ont eu tendance à rajouter des unités de calcul. Un point important est que la quadruple émission permet de nombreuses possibilités, mais la plupart ne sont pas intéressantes. Pour donner un exemple concret, prenons un CPU superscalaire très simple, comprenant une ALU entière, une FPU, une unité de branchement, et une unité mémoire. Il est possible de créer un CPU quadruple émission avec ces unités, mais le gain en performance n'en vaudrait pas la peine pour le cout en circuit. Il est rare qu'un bloc de 4 instructions contienne exactement une opération entière, une flottante, un branchement et un accès mémoire. En pratique, les seules combinaisons fréquentes sont les combinaisons calcul + accès mémoire et calcul + branchement. Les combinaisons calcul entier + calcul flottant sont aussi possibles, mais plus rares. Mais on dépasse rarement la double émission. Il est préférable d'utiliser ces unités dans un CPU double ou triple émission, en retirant l'unité de branchement et/ou en couplant l'unité mémoire à l'ALU. En clair, une fois arrivé à la quadruple émission, on ne peut plus se contenter d'une ALU, d'une FPU, d'une unité de branchement et d'une unité mémoire. Les processeurs superscalaires larges n'ont pas le choix que de rajouter des unités de calcul, voire des unités mémoire. Et en pratique, ils ont privilégié l'ajout d'ALU entières, avant de dupliquer les autres circuits. La raison est que les programmes informatiques utilisent beaucoup plus d'additions/soustractions/comparaisons que le reste. Il y a environ 5 opérations entières de base pour une multiplication entière, le ratio est le même quand on remplace la multiplication par un branchement ou un accès mémoire. En conséquence, les ALU entières ont été dupliquées en premier, et dans de nombreux exemplaires. Reste à voir comment a été dupliqué le reste. Les multiplieurs n'ont pas été dupliqués, sauf sur les processeurs superscalaires très larges. La raison est qu'il est rare de devoir exécuter deux multiplications à la fois. C'est la même chose pour le ''barrel shifter''. Deux branchements consécutifs sont quant à eux plus fréquents, c'est même assez courant. Aussi, les unités de branchement ont été dupliquées en second lieu, après les ALU entières. Avoir plusieurs accès mémoire proches est assez courant, surtout sur les CPU de type CISC. Aussi, les unités mémoire ont été dupliquées en troisième lieu. Pour résumer, dupliquer des ALU entières est donc la priorité, suivi par la duplication des unités de branchement, puis celle des accès mémoire, pour terminer par le multiplieur. La FPU est un peu à part. Une autre tendance a été de séparer l'unité mémoire des ALU entières, ce qui a permis l''''émission parallèle des µops mémoire'''. Comme son nom l'indique, l'idée est de séparer les µops mémoire des opérations entières. Il devient alors possible d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière. La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple. La '''double émission flottante''' a été utilisée sur les processeurs Alpha. L'idée est d'émettre deux opérations flottantes à la fois. Sauf qu'une implémentation naïve demanderait de dupliquer la FPU, ce qui aurait un cout en circuit rédhibitoire. Pour rappel, les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant. L'idée est d'avoir des ports d'émission séparés pour l'additionneur flottant et le multiplieur flottant. Le processeur peut alors émettre une addition flottante en même temps qu'une multiplication flottante. Les autres circuits de calcul flottant sont répartis sur ces deux ports d'émission flottants. L'avantage est que cela se marie bien avec l'usage d'une fenêtre d'instruction séparée pour les opérations flottantes. La fenêtre d'instruction a alors deux ports séparés, au lieu d'un seul. Rajouter un second port d'émission flottant n'est pas trop un problème, car le cout lié à l'ajout d'un port n'est pas linéaire. Passer de un port à deux a un cout tolérable, bien plus que de passer de 3 ports à 4 ou de 4 à 5. Il est possible de dupliquer l'additionneur et le multiplieur flottant, ce qui permet d'émettre deux additions et multiplications flottantes en même temps. C'est ce qui est fait sur les processeurs AMD d'architecture Zen 1 et 2. Ils ont deux additionneurs flottants par cœur, deux multiplieurs flottants, chacun avec leur propre port d'émission. Les performances en calcul flottant sont assez impressionnantes pour un processeur de l'époque. [[File:ZEN - émission multiple flottante.png|centre|vignette|upright=2|Microarchitecture Zen 1 d'AMD.]] ==Les ports d'émission et de décodage== Les ports d'émission et de décodage sont des ressources matérielles comme les autres. Dans cette section, nous allons parler de concepts liés aux ports d'émission et de décodage. Nous allons notamment parler du partage des ports d'émission, ainsi que des relations numériques entre ces deux types de ports. ===Le partage des ports d'émission et la ''port contention''=== Il est possible de partager un port d'émission, à savoir de connecter un port d'émission à plusieurs unités de calcul. Nous en avons vu un exemple plus haut, avec l'émission multiple des opérations entières. On peut par exemple connecter un multiplieur et une ALU entière sur le même port d'émission. Pour le moment, nous n'avons vu que des cas où un port d'émission est relié soit à des ALU entières, soit à des FPU, pas aux deux. Mais il existe des processeurs où un même port d'émission alimente à la fois une ALU entière et une FPU. Par exemple, on peut relier un additionneur flottant sur le même port qu'une ALU entière. Il faut noter que cela implique une fenêtre d'instruction centralisée, capable de mettre en attente micro-opérations entières et flottantes. Un exemple est celui des processeurs Core 2 Duo. Ils disposent de 6 ports d'émission, dont 3 ports dédiés à l'unité mémoire. Les 3 ports restants alimentent chacun : une ALU entière, un circuit de calcul flottant et une unité de calcul SSE (une unité de calcul SIMD, qu'on abordera dans quelques chapitres). * Le premier port alimente une ALU entière simple et un multiplieur/diviseur flottant. * Le second alimente une ALU entière, un multiplieur entier et un additionneur flottant. * Le troisième alimente une ALU entière, sans circuit flottant dédié. Dans le même genre, il est possible de partager un port d'émission entre l'unité mémoire et une ALU entière. Cela permet d'utiliser l'ALU entière pour les calculs d'adresse, ce qui évite d'avoir à utiliser une unité de calcul d'adresse distincte. Un exemple est celui du processeur superscalaire double émission Power PC 440. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. L'organisation en question permet soit d'émettre un accès mémoire en même temps qu'une opération entière, soit d'émettre deux opérations entières simples, soit d’émettre une multiplication et une addition/soustraction/comparaison. Une organisation simple, mais très efficace ! [[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]] Regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles, qui ne se présentent que pour certaines combinaisons de micro-opérations bien précises. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port. Le nombre de ports peut être un facteur limitant pour la performance dans certaines situations de '''''port contention''''' où on a assez d'ALU pour exécuter N micro-opérations, mais où la répartition des ALUs sur les ports l’empêche. Mais on peut réduire la ''port contention'' en répartissant correctement les ALU sur les ports d'émission. Le choix en question dépendent fortement du fait qu'en moyenne, que certaines paires d'instructions sont plus fréquentes que d'autres. Tout cela guide ce choix de répartition des ALUs sur les ports. Nous venons de voir que la répartition des ALU sur les ports d'émission est très variable d'un processeur à l'autre. Les divers choix possibles sont tous des compromis entre deux forces : réduire le nombre de ports d'émission d'un côté, garder de bonnes performances en réduisant la ''port contention'' de l'autre. Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction relativement simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ? Par contre, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal réparti les ALU. ===La séparation entre largeur de décodage et d'émission=== Sur les processeurs superscalaires larges, le processeur peut émettre plus d'instructions qu'il peut en décoder. Plus haut, j'ai dit que l'unité d'émission avait parfois plus de ports d'émission que de ports de décodage. La raison est assez simple, et elle s'explique assez bien avec un exemple. Prenons un processeur avec trois ALU entières, une FPU, une unité de branchement et une unité mémoire. Chacune a son propre port d'émission, ce qui fait que le CPU est capable d'émettre 6 µops. Du moins, c'est la théorie, car les conditions pour sont assez rares. Il faut qu'en chargeant un paquet de 6 instructions, il y ait exactement trois instructions entières, une flottante, un branchement et un accès mémoire. Si le paquet contient 5 instructions entières et un branchement, seules quatre instructions sont émises. Dans cet exemple, dans la grande majorité des cas, seuls 4 ports seront utilisés en moyenne et deux seront sous-utilisés. Il s'agit là d'une moyenne, qui dépend du code, et des instructions exactes à exécuter. Cependant, cette moyenne nous donne un moyen d'économiser beaucoup de circuits pour un cout en performance mineur. L'idée est de ne charger que des blocs de 4 instructions, mais d'avoir 6 ports d'émission. Le processeur charge et décode 4 instructions, les quatre instructions sont envoyées sur les ports d'émission adéquat. Le cout en transistors est alors situé entre celui d'un CPU quadruple émission et sextuple émission. Et les performances sont aussi entre les deux. Pour les performances, c'est parce que cela permet d'avoir un CPU quadruple émission avec bien moins de contraintes d’appariement. Imaginez qu'on se soit limité à quatre ports d'émission : il aurait fallu mettre plusieurs unités sur le même port d'émission. En conséquence, il y aurait eu des combinaisons interdites, comme émettre deux instructions entières + un branchement + un accès mémoire ; ou deux instructions entières + une opération flottante + un accès mémoire. De tells appariement interdits sont possibles en utilisant les six ports d'émission. ==L'émission multiple des accès mémoire== Les premiers processeurs superscalaires ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles. Par contre, c'est autre chose sur les processeurs superscalaires larges. Vu qu'ils travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares. L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. La ''load/store queue'' reste une structure unique, mais on peut lui rajouter des ports. Et à ce peit jeu, il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire. La première duplique les unités de calcul, mais pas les ports d'accès au cache. Les accès mémoire sont alors accumulés dans la ''load/store queue'', qui sont ensuite traités un par un par le cache. Un processeur utilisant cette technique est, par exemple, capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. Un exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. Ces processeurs pouvaient calculer trois adresses simultanément, mais ne pouvaient faire qu'un seul accès au cache par cycle. Le cout en circuit est modéré : les unités de calcul coutent très peu, les ports de la ''load/store queue'' coutent déjà un peu plus. La seconde méthode ajoute des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultanées. Il faut alors dupliquer des unités de calcul, rajouter les ports sur le cache, mais aussi rajouter des ports sur la ''load/store queue'', pour qu'elle échange des données/commandes mémoire avec le cache. Le cout en circuits est alors conséquent Quelques processeurs utilisaient des solutions intermédiaires, un exemple classique étant les Pentium 1 et 2 d'Intel. Mais nous en parlerons dans le chapitre suivant. [[File:Processeur non-superscalaire basique 02.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]] Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre. Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Le Pentium 4 était dans ce cas, mais il était un peu seul dans sa catégorie. Un cas similaire est celui où l'unité mémoire est alimentée par une fenêtre de micro-opération mélangeant instructions mémoire et entières. Les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. Dans ce cas, l'émission multiple est rarement implémentée. Le cas suivant a une file pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique. Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui pouvaient seulement émettre une lecture en même temps qu'une écriture. Ils avaient deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse. [[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]] Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture demande d'utiliser une file d'écriture multiport, ce qui est assez compliqué. Implémenter des lectures en plus est plus simple : il suffit de rajouter une seconde file de µops LOAD et une unité de calcul, alimentées via un second port d'émission. Il est aussi possible de rajouter un second port de lecture au cache, mais ce n'est pas obligatoire. Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse. De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports de lecture et d'écriture car il y a plus de lecture que d'écritures. Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. : Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse. [[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]] Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettent plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail. Pour résumer, l'évolution des CPU superscalaires a été la suivante : * pipeline mémorie fusionné avec le pipeline entier ; * séparation du pipeline mémoire du reste, avec émission parallèle des accès mémoire ; * émission multiple, mais sans dupliquer les ports du cache ; * émission multiple, avec émission simultanée d'une lecture et d’une écriture dans le cache ; * émission multiple avec plusieurs accès en lecture au cache. ==Les optimisations des processeurs superscalaires larges== Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul. ===L'étape de chargement d'un CPU superscalaire large=== Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 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 autoaligné.]] Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements ! [[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]] Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez 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 microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total). Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois 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 microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement. Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations. ===La micro-fusion des processeurs Intel et AMD=== La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''. Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''. Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout. La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Le parallélisme mémoire | prevText=Le parallélisme mémoire | next=Exemples de microarchitectures CPU : le cas du x86 | nextText=Exemples de microarchitectures CPU : le cas du x86 }} </noinclude> r78856ebcdvifb4748cdzx71iocjr1n 772690 772689 2026-09-21T14:48:44Z Mewtow 31375 /* L'émission multiple des micro-opérations entières */ 772690 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 tout premiers processeurs superscalaires pouvaient émettre deux µops en même temps, ce qui portait le nom de '''double émission'''. Mais le nombre a rapidement augmenté et les processeurs modernes 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. ==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. Par exemple, l'unité de chargement charge deux fois plus d'instructions, mais n'est pas modifiée en profondeur. L'unité d'émission et les circuits d'exécution dans le désordre sont eux aussi altérés. Les unités de calcul sont souvent dupliquées, sauf sur les tout premiers CPU superscalaires. {|class="wikitable" |- ! rowspan="2" | Processeur sans émission multiple | rowspan="2" | Chargement | rowspan="2" | Décodage | rowspan="2" | Renommage | rowspan="2" | Émission | Exécution / ALU | rowspan="2" | ''Commit''/ROB |- | Exécution / ALU |- ! rowspan="4" | Processeur superscalaire | rowspan="4" | Chargement | rowspan="2" | Décodage | rowspan="4" | Renommage | rowspan="4" | Émission | Exécution / ALU | rowspan="4" | ''Commit''/ROB |- | Exécution / ALU |- | rowspan="2" | Décodage | Exécution / ALU |- | Exécution / ALU |} Voyons ce qu'il en est dans le détail. ===Les unités de calcul d'un processeur superscalaire=== Émettre N instructions simultanément demande implicitement d'exécuter N instructions par cycle, voire plus si des instructions multicycles viennent mettre leur grain de sel. Intuitivement, on se dit que les unités de calcul doivent être dupliquées. Et c'est une solution viable pour les processeurs très simples, avec seulement quelques ALU entières. Par exemple, un processeur RISC basique avec seulement une ALU entière et un ''barrel shifter'' peut être rendu superscalaire en dupliquant les deux circuits. Le problème, est que cette solution marche assez mal dès que le processeur contient des circuits multiplieurs et/ou une FPU. Par exemple, un processeur à double émission devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, etc. 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 ! Par contre, l'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission. Un autre cas extrême est celui où aucune unité de calcul n'est dupliquée. Pour comprendre comment c'est possible rappelez-vous qu'un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et éventuellement une unité de branchement. Cela suffit pour créer un processeur quadruple émission, très limité. Un tel processeur peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire. Le problème est qu'il est rare que l'on ait à exécuter en même temps un calcul entier, un calcul flottant, un accès mémoire et un branchement. La quadruple émission n'est donc que rarement utilisée. En pratique, les seules combinaisons fréquentes sont les combinaisons calcul + accès mémoire et calcul + branchement. Les combinaisons calcul entier + calcul flottant sont aussi possibles, mais plus rares. Mais on dépasse rarement la double émission. En clair, à partir de la quadruple émission, on ne peut plus se contenter d'une ALU, d'une FPU, d'une unité de branchement et d'une unité mémoire. Les processeurs superscalaires larges n'ont pas le choix que de rajouter des unités de calcul. Reste à voir ce qui est dupliqué en pratique. 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. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant. La conséquence est que les processeurs superscalaires ont des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a des paires d'instructions autorisées et des paires interdites. Par exemple, l'exécution simultanée de deux branchements est interdite, les branchements sont exécutés l'un après l'autre. Mais il est possible d'émettre un branchement en même temps qu'une autre instruction, en espérant que la prédiction de branchement ait fait une bonne prédiction. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur. En pratique, ces contraintes d’appariement ont peu d’impact. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière, le ratio est le même quand on remplace la multiplication par un branchement ou un accès mémoire. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Deux branchements consécutifs sont quant à eux plus fréquents, idem pour deux accès mémoire. Dupliquer des ALU entières est donc la priorité, suivi par la duplication des unités de branchement, puis celle des accès mémoire, pour terminer par le multiplieur et la FPU. ===Le chargement de plusieurs instructions consécutives=== Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large. Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence. Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas. Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement non-pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc. [[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]] ===Le décodage parallèle des instructions=== Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits. Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur. Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! ===L'unité d'émission d'un processeur superscalaire=== La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. Nous verrons pourquoi dans la suite, mais on peut dire que c'est juste plus simple et plus efficace ainsi. Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop candidate, sous entendu "candidate à l'émission". L'unité d'émission détecte les dépendances entre la µop candidate et les µops en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en cours d'exécution dans le pipeline. mais en plus, elle doit tester les dépendances entre les µops candidates. Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément, il doit détecter les paires d'instructions dépendantes et les sérialiser. Avec une fenêtre d'instruction, il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil gérent les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires modernes utilisent tous une fenêtre d'instruction centralisée ou décentralisée, et non des ''scoreboard''. Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps, là encore en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire peut émettre 8 µops à la fois, on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle. ===L'unité de renommage superscalaire=== Avec la superscalarité, l'unité de renommage de registre doit gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance de registre, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées. Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat. [[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]] Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante. [[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]] Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande. [[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]] ===Le banc de registre d'un processeur superscalaire=== Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement. 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. ==L'évolution historique des processeurs superscalaires== Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs triple émission permettent eux d’émettre trois instructions à la fois. Les tout premiers processeurs superscalaires avaient un budget en transistors très limité, ce qui fait qu'ils ne pouvaient pas être "totalement superscalaires". Par exemple, un processeur double émission ne pouvait pas émettre deux opérations entières en même temps, ni deux opérations flottantes, ni une opération entière avec un accès mémoire. Il y avait des paires d'instructions autorisées et des paires interdites. Les '''contraintes d'appariement''' sur les instructions à émettre en même temps étaient très strictes. L'intérêt de ces contraintes d’appariement est que le processeur était très simple, il n'y avait pas besoin de rajouter des circuits complexes pour rendre le processeur superscalaire. Les CPU superscalaires historiques ne faisaient que mieux utiliser des unités de calcul déjà présentes. Prenons un processeur non-superscalaire simple, illustré ci-dessous. Il contient : une unité de chargement, un décodeur, une unité d'émission, une ALU, une FPU, les bancs de registre entier et flottants, et une unité mémoire. Nous avons placé l'unité mémoire à la suite de l'ALU entière, car ce sera important pour les explications qui vont suivre. [[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]] Les processeurs superscalaires historiques modifiaient assez peu le schéma précédent. L'unité de chargement était adaptée pour lire plusieurs instructions depuis la mémoire, il y avait un seul décodeur, l'unité d'émission était assez peu modifiée. Mais une chose était certaine : aucune unité de calcul n'était dupliquée, le banc de registre n'était presque pas modifié. A la rigueur, gérer la triple 'émission avec les branchements demandait parfois d'ajouter des ports de lecture sur le banc de registre généraux, pas plus. ===La double émission entière-flottante=== Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est d'émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Le processeur a donc deux pipelines séparés : un pour les micro-opérations entières, un autre pour les micro-opérations flottantes. On parle alors de '''double émission entière-flottante'''. {|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, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité. La plupart des premiers processeurs superscalaires étaient de ce type. Les exemples les plus notables sont les processeurs POWER 1 et ses dérivés comme le ''RISC Single Chip''. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle. [[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]] La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisations visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate]. ===L'émission parallèle des branchements=== Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur. 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 modifier le banc de registres. Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. [[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]] Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications et les divisions ? Pour étudier ces questions, nous allons prendre le cas d'un processeur capable d’émettre trois opérations en même temps : deux opérations entières, et une flottante, potentiellement d'autres. {|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 |} 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|Emission multiple des opérations entières, implémentation courante]] Pour les branchements, deux solutions sont possibles. Avec la première, il y a émission parallèle des branchements, à savoir qu'il est possible d'émettre en branchement en parallèle de deux-trois instructions entières. Et cela impose la présence d'une unité de branchement séparée, avec son port d'émission. Avec la seconde, les branchements sont traités comme des instructions entières. Soit les branchements sont exécutés dans une ALU entière, soit il y a une unité de branchement reliée à un port d'émission pour les opérations entières. [[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Emission série et parallèle des branchements]] Avec la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales, sur les processeurs superscalaires historiques. En clair, le processeur peut émettre 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. De plus, 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. Ce compromis change cependant quand on passe à la quadruple émission. {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 |- | * ALU entière * Multiplieur | * ALU entière * Barrel shifter * Unité de branchement * Unité mémoire | FPU |- | * Additions, soustractions, opérations bit à bit, etc. * Multiplication | * Additions, soustractions, opérations bit à bit, etc. * Décalages et rotations * Branchements * Accès mémoire |Opération flottante |} ===Les CPU superscalaires à plusieurs unités spécialisées=== Le classique deux opérations entières + une flottante était l'idéal pour la triple émission. Même les processeurs modernes à triple émission restent sur cette "norme", car elle est proche de l'optimal. Mais arrivé à la quadruple émission, les possibilités sont devenues plus diverses. L'évolution des CPU superscalaires ont évolués dans des directions différentes, chacun faisant à sa sauce, les choses sont devenues plus confuses. Le seul point commun est qu'ils ont eu tendance à rajouter des unités de calcul. Un point important est que la quadruple émission permet de nombreuses possibilités, mais la plupart ne sont pas intéressantes. Pour donner un exemple concret, prenons un CPU superscalaire très simple, comprenant une ALU entière, une FPU, une unité de branchement, et une unité mémoire. Il est possible de créer un CPU quadruple émission avec ces unités, mais le gain en performance n'en vaudrait pas la peine pour le cout en circuit. Il est rare qu'un bloc de 4 instructions contienne exactement une opération entière, une flottante, un branchement et un accès mémoire. En pratique, les seules combinaisons fréquentes sont les combinaisons calcul + accès mémoire et calcul + branchement. Les combinaisons calcul entier + calcul flottant sont aussi possibles, mais plus rares. Mais on dépasse rarement la double émission. Il est préférable d'utiliser ces unités dans un CPU double ou triple émission, en retirant l'unité de branchement et/ou en couplant l'unité mémoire à l'ALU. En clair, une fois arrivé à la quadruple émission, on ne peut plus se contenter d'une ALU, d'une FPU, d'une unité de branchement et d'une unité mémoire. Les processeurs superscalaires larges n'ont pas le choix que de rajouter des unités de calcul, voire des unités mémoire. Et en pratique, ils ont privilégié l'ajout d'ALU entières, avant de dupliquer les autres circuits. La raison est que les programmes informatiques utilisent beaucoup plus d'additions/soustractions/comparaisons que le reste. Il y a environ 5 opérations entières de base pour une multiplication entière, le ratio est le même quand on remplace la multiplication par un branchement ou un accès mémoire. En conséquence, les ALU entières ont été dupliquées en premier, et dans de nombreux exemplaires. Reste à voir comment a été dupliqué le reste. Les multiplieurs n'ont pas été dupliqués, sauf sur les processeurs superscalaires très larges. La raison est qu'il est rare de devoir exécuter deux multiplications à la fois. C'est la même chose pour le ''barrel shifter''. Deux branchements consécutifs sont quant à eux plus fréquents, c'est même assez courant. Aussi, les unités de branchement ont été dupliquées en second lieu, après les ALU entières. Avoir plusieurs accès mémoire proches est assez courant, surtout sur les CPU de type CISC. Aussi, les unités mémoire ont été dupliquées en troisième lieu. Pour résumer, dupliquer des ALU entières est donc la priorité, suivi par la duplication des unités de branchement, puis celle des accès mémoire, pour terminer par le multiplieur. La FPU est un peu à part. Une autre tendance a été de séparer l'unité mémoire des ALU entières, ce qui a permis l''''émission parallèle des µops mémoire'''. Comme son nom l'indique, l'idée est de séparer les µops mémoire des opérations entières. Il devient alors possible d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière. La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple. La '''double émission flottante''' a été utilisée sur les processeurs Alpha. L'idée est d'émettre deux opérations flottantes à la fois. Sauf qu'une implémentation naïve demanderait de dupliquer la FPU, ce qui aurait un cout en circuit rédhibitoire. Pour rappel, les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant. L'idée est d'avoir des ports d'émission séparés pour l'additionneur flottant et le multiplieur flottant. Le processeur peut alors émettre une addition flottante en même temps qu'une multiplication flottante. Les autres circuits de calcul flottant sont répartis sur ces deux ports d'émission flottants. L'avantage est que cela se marie bien avec l'usage d'une fenêtre d'instruction séparée pour les opérations flottantes. La fenêtre d'instruction a alors deux ports séparés, au lieu d'un seul. Rajouter un second port d'émission flottant n'est pas trop un problème, car le cout lié à l'ajout d'un port n'est pas linéaire. Passer de un port à deux a un cout tolérable, bien plus que de passer de 3 ports à 4 ou de 4 à 5. Il est possible de dupliquer l'additionneur et le multiplieur flottant, ce qui permet d'émettre deux additions et multiplications flottantes en même temps. C'est ce qui est fait sur les processeurs AMD d'architecture Zen 1 et 2. Ils ont deux additionneurs flottants par cœur, deux multiplieurs flottants, chacun avec leur propre port d'émission. Les performances en calcul flottant sont assez impressionnantes pour un processeur de l'époque. [[File:ZEN - émission multiple flottante.png|centre|vignette|upright=2|Microarchitecture Zen 1 d'AMD.]] ==Les ports d'émission et de décodage== Les ports d'émission et de décodage sont des ressources matérielles comme les autres. Dans cette section, nous allons parler de concepts liés aux ports d'émission et de décodage. Nous allons notamment parler du partage des ports d'émission, ainsi que des relations numériques entre ces deux types de ports. ===Le partage des ports d'émission et la ''port contention''=== Il est possible de partager un port d'émission, à savoir de connecter un port d'émission à plusieurs unités de calcul. Nous en avons vu un exemple plus haut, avec l'émission multiple des opérations entières. On peut par exemple connecter un multiplieur et une ALU entière sur le même port d'émission. Pour le moment, nous n'avons vu que des cas où un port d'émission est relié soit à des ALU entières, soit à des FPU, pas aux deux. Mais il existe des processeurs où un même port d'émission alimente à la fois une ALU entière et une FPU. Par exemple, on peut relier un additionneur flottant sur le même port qu'une ALU entière. Il faut noter que cela implique une fenêtre d'instruction centralisée, capable de mettre en attente micro-opérations entières et flottantes. Un exemple est celui des processeurs Core 2 Duo. Ils disposent de 6 ports d'émission, dont 3 ports dédiés à l'unité mémoire. Les 3 ports restants alimentent chacun : une ALU entière, un circuit de calcul flottant et une unité de calcul SSE (une unité de calcul SIMD, qu'on abordera dans quelques chapitres). * Le premier port alimente une ALU entière simple et un multiplieur/diviseur flottant. * Le second alimente une ALU entière, un multiplieur entier et un additionneur flottant. * Le troisième alimente une ALU entière, sans circuit flottant dédié. Dans le même genre, il est possible de partager un port d'émission entre l'unité mémoire et une ALU entière. Cela permet d'utiliser l'ALU entière pour les calculs d'adresse, ce qui évite d'avoir à utiliser une unité de calcul d'adresse distincte. Un exemple est celui du processeur superscalaire double émission Power PC 440. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. L'organisation en question permet soit d'émettre un accès mémoire en même temps qu'une opération entière, soit d'émettre deux opérations entières simples, soit d’émettre une multiplication et une addition/soustraction/comparaison. Une organisation simple, mais très efficace ! [[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]] Regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles, qui ne se présentent que pour certaines combinaisons de micro-opérations bien précises. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port. Le nombre de ports peut être un facteur limitant pour la performance dans certaines situations de '''''port contention''''' où on a assez d'ALU pour exécuter N micro-opérations, mais où la répartition des ALUs sur les ports l’empêche. Mais on peut réduire la ''port contention'' en répartissant correctement les ALU sur les ports d'émission. Le choix en question dépendent fortement du fait qu'en moyenne, que certaines paires d'instructions sont plus fréquentes que d'autres. Tout cela guide ce choix de répartition des ALUs sur les ports. Nous venons de voir que la répartition des ALU sur les ports d'émission est très variable d'un processeur à l'autre. Les divers choix possibles sont tous des compromis entre deux forces : réduire le nombre de ports d'émission d'un côté, garder de bonnes performances en réduisant la ''port contention'' de l'autre. Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction relativement simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ? Par contre, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal réparti les ALU. ===La séparation entre largeur de décodage et d'émission=== Sur les processeurs superscalaires larges, le processeur peut émettre plus d'instructions qu'il peut en décoder. Plus haut, j'ai dit que l'unité d'émission avait parfois plus de ports d'émission que de ports de décodage. La raison est assez simple, et elle s'explique assez bien avec un exemple. Prenons un processeur avec trois ALU entières, une FPU, une unité de branchement et une unité mémoire. Chacune a son propre port d'émission, ce qui fait que le CPU est capable d'émettre 6 µops. Du moins, c'est la théorie, car les conditions pour sont assez rares. Il faut qu'en chargeant un paquet de 6 instructions, il y ait exactement trois instructions entières, une flottante, un branchement et un accès mémoire. Si le paquet contient 5 instructions entières et un branchement, seules quatre instructions sont émises. Dans cet exemple, dans la grande majorité des cas, seuls 4 ports seront utilisés en moyenne et deux seront sous-utilisés. Il s'agit là d'une moyenne, qui dépend du code, et des instructions exactes à exécuter. Cependant, cette moyenne nous donne un moyen d'économiser beaucoup de circuits pour un cout en performance mineur. L'idée est de ne charger que des blocs de 4 instructions, mais d'avoir 6 ports d'émission. Le processeur charge et décode 4 instructions, les quatre instructions sont envoyées sur les ports d'émission adéquat. Le cout en transistors est alors situé entre celui d'un CPU quadruple émission et sextuple émission. Et les performances sont aussi entre les deux. Pour les performances, c'est parce que cela permet d'avoir un CPU quadruple émission avec bien moins de contraintes d’appariement. Imaginez qu'on se soit limité à quatre ports d'émission : il aurait fallu mettre plusieurs unités sur le même port d'émission. En conséquence, il y aurait eu des combinaisons interdites, comme émettre deux instructions entières + un branchement + un accès mémoire ; ou deux instructions entières + une opération flottante + un accès mémoire. De tells appariement interdits sont possibles en utilisant les six ports d'émission. ==L'émission multiple des accès mémoire== Les premiers processeurs superscalaires ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles. Par contre, c'est autre chose sur les processeurs superscalaires larges. Vu qu'ils travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares. L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. La ''load/store queue'' reste une structure unique, mais on peut lui rajouter des ports. Et à ce peit jeu, il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire. La première duplique les unités de calcul, mais pas les ports d'accès au cache. Les accès mémoire sont alors accumulés dans la ''load/store queue'', qui sont ensuite traités un par un par le cache. Un processeur utilisant cette technique est, par exemple, capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. Un exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. Ces processeurs pouvaient calculer trois adresses simultanément, mais ne pouvaient faire qu'un seul accès au cache par cycle. Le cout en circuit est modéré : les unités de calcul coutent très peu, les ports de la ''load/store queue'' coutent déjà un peu plus. La seconde méthode ajoute des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultanées. Il faut alors dupliquer des unités de calcul, rajouter les ports sur le cache, mais aussi rajouter des ports sur la ''load/store queue'', pour qu'elle échange des données/commandes mémoire avec le cache. Le cout en circuits est alors conséquent Quelques processeurs utilisaient des solutions intermédiaires, un exemple classique étant les Pentium 1 et 2 d'Intel. Mais nous en parlerons dans le chapitre suivant. [[File:Processeur non-superscalaire basique 02.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]] Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre. Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Le Pentium 4 était dans ce cas, mais il était un peu seul dans sa catégorie. Un cas similaire est celui où l'unité mémoire est alimentée par une fenêtre de micro-opération mélangeant instructions mémoire et entières. Les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. Dans ce cas, l'émission multiple est rarement implémentée. Le cas suivant a une file pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique. Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui pouvaient seulement émettre une lecture en même temps qu'une écriture. Ils avaient deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse. [[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]] Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture demande d'utiliser une file d'écriture multiport, ce qui est assez compliqué. Implémenter des lectures en plus est plus simple : il suffit de rajouter une seconde file de µops LOAD et une unité de calcul, alimentées via un second port d'émission. Il est aussi possible de rajouter un second port de lecture au cache, mais ce n'est pas obligatoire. Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse. De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports de lecture et d'écriture car il y a plus de lecture que d'écritures. Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. : Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse. [[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]] Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettent plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail. Pour résumer, l'évolution des CPU superscalaires a été la suivante : * pipeline mémorie fusionné avec le pipeline entier ; * séparation du pipeline mémoire du reste, avec émission parallèle des accès mémoire ; * émission multiple, mais sans dupliquer les ports du cache ; * émission multiple, avec émission simultanée d'une lecture et d’une écriture dans le cache ; * émission multiple avec plusieurs accès en lecture au cache. ==Les optimisations des processeurs superscalaires larges== Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul. ===L'étape de chargement d'un CPU superscalaire large=== Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 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 autoaligné.]] Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements ! [[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]] Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez 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 microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total). Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois 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 microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement. Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations. ===La micro-fusion des processeurs Intel et AMD=== La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''. Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''. Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout. La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Le parallélisme mémoire | prevText=Le parallélisme mémoire | next=Exemples de microarchitectures CPU : le cas du x86 | nextText=Exemples de microarchitectures CPU : le cas du x86 }} </noinclude> o9eojj7ef33ns65a24bmcfpgx7rwmve 772691 772690 2026-09-21T14:52:42Z Mewtow 31375 /* Les CPU superscalaires à plusieurs unités spécialisées */ 772691 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 tout premiers processeurs superscalaires pouvaient émettre deux µops en même temps, ce qui portait le nom de '''double émission'''. Mais le nombre a rapidement augmenté et les processeurs modernes 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. ==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. Par exemple, l'unité de chargement charge deux fois plus d'instructions, mais n'est pas modifiée en profondeur. L'unité d'émission et les circuits d'exécution dans le désordre sont eux aussi altérés. Les unités de calcul sont souvent dupliquées, sauf sur les tout premiers CPU superscalaires. {|class="wikitable" |- ! rowspan="2" | Processeur sans émission multiple | rowspan="2" | Chargement | rowspan="2" | Décodage | rowspan="2" | Renommage | rowspan="2" | Émission | Exécution / ALU | rowspan="2" | ''Commit''/ROB |- | Exécution / ALU |- ! rowspan="4" | Processeur superscalaire | rowspan="4" | Chargement | rowspan="2" | Décodage | rowspan="4" | Renommage | rowspan="4" | Émission | Exécution / ALU | rowspan="4" | ''Commit''/ROB |- | Exécution / ALU |- | rowspan="2" | Décodage | Exécution / ALU |- | Exécution / ALU |} Voyons ce qu'il en est dans le détail. ===Les unités de calcul d'un processeur superscalaire=== Émettre N instructions simultanément demande implicitement d'exécuter N instructions par cycle, voire plus si des instructions multicycles viennent mettre leur grain de sel. Intuitivement, on se dit que les unités de calcul doivent être dupliquées. Et c'est une solution viable pour les processeurs très simples, avec seulement quelques ALU entières. Par exemple, un processeur RISC basique avec seulement une ALU entière et un ''barrel shifter'' peut être rendu superscalaire en dupliquant les deux circuits. Le problème, est que cette solution marche assez mal dès que le processeur contient des circuits multiplieurs et/ou une FPU. Par exemple, un processeur à double émission devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, etc. 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 ! Par contre, l'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission. Un autre cas extrême est celui où aucune unité de calcul n'est dupliquée. Pour comprendre comment c'est possible rappelez-vous qu'un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et éventuellement une unité de branchement. Cela suffit pour créer un processeur quadruple émission, très limité. Un tel processeur peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire. Le problème est qu'il est rare que l'on ait à exécuter en même temps un calcul entier, un calcul flottant, un accès mémoire et un branchement. La quadruple émission n'est donc que rarement utilisée. En pratique, les seules combinaisons fréquentes sont les combinaisons calcul + accès mémoire et calcul + branchement. Les combinaisons calcul entier + calcul flottant sont aussi possibles, mais plus rares. Mais on dépasse rarement la double émission. En clair, à partir de la quadruple émission, on ne peut plus se contenter d'une ALU, d'une FPU, d'une unité de branchement et d'une unité mémoire. Les processeurs superscalaires larges n'ont pas le choix que de rajouter des unités de calcul. Reste à voir ce qui est dupliqué en pratique. 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. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant. La conséquence est que les processeurs superscalaires ont des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a des paires d'instructions autorisées et des paires interdites. Par exemple, l'exécution simultanée de deux branchements est interdite, les branchements sont exécutés l'un après l'autre. Mais il est possible d'émettre un branchement en même temps qu'une autre instruction, en espérant que la prédiction de branchement ait fait une bonne prédiction. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur. En pratique, ces contraintes d’appariement ont peu d’impact. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière, le ratio est le même quand on remplace la multiplication par un branchement ou un accès mémoire. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Deux branchements consécutifs sont quant à eux plus fréquents, idem pour deux accès mémoire. Dupliquer des ALU entières est donc la priorité, suivi par la duplication des unités de branchement, puis celle des accès mémoire, pour terminer par le multiplieur et la FPU. ===Le chargement de plusieurs instructions consécutives=== Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large. Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence. Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas. Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement non-pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc. [[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]] ===Le décodage parallèle des instructions=== Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits. Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur. Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! ===L'unité d'émission d'un processeur superscalaire=== La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. Nous verrons pourquoi dans la suite, mais on peut dire que c'est juste plus simple et plus efficace ainsi. Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop candidate, sous entendu "candidate à l'émission". L'unité d'émission détecte les dépendances entre la µop candidate et les µops en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en cours d'exécution dans le pipeline. mais en plus, elle doit tester les dépendances entre les µops candidates. Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément, il doit détecter les paires d'instructions dépendantes et les sérialiser. Avec une fenêtre d'instruction, il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil gérent les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires modernes utilisent tous une fenêtre d'instruction centralisée ou décentralisée, et non des ''scoreboard''. Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps, là encore en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire peut émettre 8 µops à la fois, on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle. ===L'unité de renommage superscalaire=== Avec la superscalarité, l'unité de renommage de registre doit gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance de registre, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées. Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat. [[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]] Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante. [[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]] Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande. [[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]] ===Le banc de registre d'un processeur superscalaire=== Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement. 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. ==L'évolution historique des processeurs superscalaires== Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs triple émission permettent eux d’émettre trois instructions à la fois. Les tout premiers processeurs superscalaires avaient un budget en transistors très limité, ce qui fait qu'ils ne pouvaient pas être "totalement superscalaires". Par exemple, un processeur double émission ne pouvait pas émettre deux opérations entières en même temps, ni deux opérations flottantes, ni une opération entière avec un accès mémoire. Il y avait des paires d'instructions autorisées et des paires interdites. Les '''contraintes d'appariement''' sur les instructions à émettre en même temps étaient très strictes. L'intérêt de ces contraintes d’appariement est que le processeur était très simple, il n'y avait pas besoin de rajouter des circuits complexes pour rendre le processeur superscalaire. Les CPU superscalaires historiques ne faisaient que mieux utiliser des unités de calcul déjà présentes. Prenons un processeur non-superscalaire simple, illustré ci-dessous. Il contient : une unité de chargement, un décodeur, une unité d'émission, une ALU, une FPU, les bancs de registre entier et flottants, et une unité mémoire. Nous avons placé l'unité mémoire à la suite de l'ALU entière, car ce sera important pour les explications qui vont suivre. [[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]] Les processeurs superscalaires historiques modifiaient assez peu le schéma précédent. L'unité de chargement était adaptée pour lire plusieurs instructions depuis la mémoire, il y avait un seul décodeur, l'unité d'émission était assez peu modifiée. Mais une chose était certaine : aucune unité de calcul n'était dupliquée, le banc de registre n'était presque pas modifié. A la rigueur, gérer la triple 'émission avec les branchements demandait parfois d'ajouter des ports de lecture sur le banc de registre généraux, pas plus. ===La double émission entière-flottante=== Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est d'émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Le processeur a donc deux pipelines séparés : un pour les micro-opérations entières, un autre pour les micro-opérations flottantes. On parle alors de '''double émission entière-flottante'''. {|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, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité. La plupart des premiers processeurs superscalaires étaient de ce type. Les exemples les plus notables sont les processeurs POWER 1 et ses dérivés comme le ''RISC Single Chip''. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle. [[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]] La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisations visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate]. ===L'émission parallèle des branchements=== Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur. 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 modifier le banc de registres. Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. [[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]] Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications et les divisions ? Pour étudier ces questions, nous allons prendre le cas d'un processeur capable d’émettre trois opérations en même temps : deux opérations entières, et une flottante, potentiellement d'autres. {|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 |} 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|Emission multiple des opérations entières, implémentation courante]] Pour les branchements, deux solutions sont possibles. Avec la première, il y a émission parallèle des branchements, à savoir qu'il est possible d'émettre en branchement en parallèle de deux-trois instructions entières. Et cela impose la présence d'une unité de branchement séparée, avec son port d'émission. Avec la seconde, les branchements sont traités comme des instructions entières. Soit les branchements sont exécutés dans une ALU entière, soit il y a une unité de branchement reliée à un port d'émission pour les opérations entières. [[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Emission série et parallèle des branchements]] Avec la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales, sur les processeurs superscalaires historiques. En clair, le processeur peut émettre 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. De plus, 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. Ce compromis change cependant quand on passe à la quadruple émission. {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 |- | * ALU entière * Multiplieur | * ALU entière * Barrel shifter * Unité de branchement * Unité mémoire | FPU |- | * Additions, soustractions, opérations bit à bit, etc. * Multiplication | * Additions, soustractions, opérations bit à bit, etc. * Décalages et rotations * Branchements * Accès mémoire |Opération flottante |} ===Les CPU superscalaires à plusieurs unités spécialisées=== Le classique deux opérations entières + une flottante était l'idéal pour la triple émission. Même les processeurs modernes à triple émission restent sur cette "norme", car elle est proche de l'optimal. Mais arrivé à la quadruple émission, les possibilités sont devenues plus diverses. Les CPU superscalaires ont évolués dans des directions différentes, chacun faisant à sa sauce. Le seul point commun est qu'ils ont eu tendance à rajouter des unités de calcul entières. La quadruple émission permet de nombreuses possibilités, mais la plupart ne sont pas intéressantes. Pour donner un exemple concret, prenons un CPU superscalaire avec une ALU entière, une FPU, une unité de branchement, et une unité mémoire. Il est possible de créer un CPU quadruple émission avec ces unités, mais le gain en performance n'en vaudrait pas la peine. Il est rare qu'un bloc de 4 instructions contienne exactement une opération entière, une flottante, un branchement et un accès mémoire. En pratique, les seules combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. En clair, on dépasse rarement la double émission. En conséquence, une fois arrivé à la quadruple émission, les processeurs superscalaires n'ont pas le choix que de rajouter des unités de calcul, voire des unités mémoire. Et en pratique, ils ont privilégié l'ajout d'ALU entières, avant de dupliquer les autres circuits. La raison est que les programmes informatiques utilisent beaucoup plus d'additions/soustractions/comparaisons que le reste. Il y a environ 5 opérations entières de base pour une multiplication entière, le ratio est le même quand on remplace la multiplication par un branchement ou un accès mémoire. En conséquence, les ALU entières ont été dupliquées en premier, et dans de nombreux exemplaires. Reste à voir comment a été dupliqué le reste. Les multiplieurs n'ont pas été dupliqués, sauf sur les processeurs superscalaires très larges. La raison est qu'il est rare de devoir exécuter deux multiplications à la fois. C'est la même chose pour le ''barrel shifter''. Deux branchements consécutifs sont quant à eux plus fréquents, c'est même assez courant. Aussi, les unités de branchement ont été dupliquées en second lieu, après les ALU entières. Avoir plusieurs accès mémoire proches est assez courant, surtout sur les CPU de type CISC. Aussi, les unités mémoire ont été dupliquées en troisième lieu. Pour résumer, dupliquer des ALU entières est donc la priorité, suivi par la duplication des unités de branchement, puis celle des accès mémoire, pour terminer par le multiplieur. La FPU est un peu à part. Une autre tendance a été de séparer l'unité mémoire des ALU entières, ce qui a permis l''''émission parallèle des µops mémoire'''. Comme son nom l'indique, l'idée est de séparer les µops mémoire des opérations entières. Il devient alors possible d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière. La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple. La '''double émission flottante''' a été utilisée sur les processeurs Alpha. L'idée est d'émettre deux opérations flottantes à la fois. Sauf qu'une implémentation naïve demanderait de dupliquer la FPU, ce qui aurait un cout en circuit rédhibitoire. Pour rappel, les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant. L'idée est d'avoir des ports d'émission séparés pour l'additionneur flottant et le multiplieur flottant. Le processeur peut alors émettre une addition flottante en même temps qu'une multiplication flottante. Les autres circuits de calcul flottant sont répartis sur ces deux ports d'émission flottants. L'avantage est que cela se marie bien avec l'usage d'une fenêtre d'instruction séparée pour les opérations flottantes. La fenêtre d'instruction a alors deux ports séparés, au lieu d'un seul. Rajouter un second port d'émission flottant n'est pas trop un problème, car le cout lié à l'ajout d'un port n'est pas linéaire. Passer de un port à deux a un cout tolérable, bien plus que de passer de 3 ports à 4 ou de 4 à 5. Il est possible de dupliquer l'additionneur et le multiplieur flottant, ce qui permet d'émettre deux additions et multiplications flottantes en même temps. C'est ce qui est fait sur les processeurs AMD d'architecture Zen 1 et 2. Ils ont deux additionneurs flottants par cœur, deux multiplieurs flottants, chacun avec leur propre port d'émission. Les performances en calcul flottant sont assez impressionnantes pour un processeur de l'époque. [[File:ZEN - émission multiple flottante.png|centre|vignette|upright=2|Microarchitecture Zen 1 d'AMD.]] ==Les ports d'émission et de décodage== Les ports d'émission et de décodage sont des ressources matérielles comme les autres. Dans cette section, nous allons parler de concepts liés aux ports d'émission et de décodage. Nous allons notamment parler du partage des ports d'émission, ainsi que des relations numériques entre ces deux types de ports. ===Le partage des ports d'émission et la ''port contention''=== Il est possible de partager un port d'émission, à savoir de connecter un port d'émission à plusieurs unités de calcul. Nous en avons vu un exemple plus haut, avec l'émission multiple des opérations entières. On peut par exemple connecter un multiplieur et une ALU entière sur le même port d'émission. Pour le moment, nous n'avons vu que des cas où un port d'émission est relié soit à des ALU entières, soit à des FPU, pas aux deux. Mais il existe des processeurs où un même port d'émission alimente à la fois une ALU entière et une FPU. Par exemple, on peut relier un additionneur flottant sur le même port qu'une ALU entière. Il faut noter que cela implique une fenêtre d'instruction centralisée, capable de mettre en attente micro-opérations entières et flottantes. Un exemple est celui des processeurs Core 2 Duo. Ils disposent de 6 ports d'émission, dont 3 ports dédiés à l'unité mémoire. Les 3 ports restants alimentent chacun : une ALU entière, un circuit de calcul flottant et une unité de calcul SSE (une unité de calcul SIMD, qu'on abordera dans quelques chapitres). * Le premier port alimente une ALU entière simple et un multiplieur/diviseur flottant. * Le second alimente une ALU entière, un multiplieur entier et un additionneur flottant. * Le troisième alimente une ALU entière, sans circuit flottant dédié. Dans le même genre, il est possible de partager un port d'émission entre l'unité mémoire et une ALU entière. Cela permet d'utiliser l'ALU entière pour les calculs d'adresse, ce qui évite d'avoir à utiliser une unité de calcul d'adresse distincte. Un exemple est celui du processeur superscalaire double émission Power PC 440. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. L'organisation en question permet soit d'émettre un accès mémoire en même temps qu'une opération entière, soit d'émettre deux opérations entières simples, soit d’émettre une multiplication et une addition/soustraction/comparaison. Une organisation simple, mais très efficace ! [[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]] Regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles, qui ne se présentent que pour certaines combinaisons de micro-opérations bien précises. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port. Le nombre de ports peut être un facteur limitant pour la performance dans certaines situations de '''''port contention''''' où on a assez d'ALU pour exécuter N micro-opérations, mais où la répartition des ALUs sur les ports l’empêche. Mais on peut réduire la ''port contention'' en répartissant correctement les ALU sur les ports d'émission. Le choix en question dépendent fortement du fait qu'en moyenne, que certaines paires d'instructions sont plus fréquentes que d'autres. Tout cela guide ce choix de répartition des ALUs sur les ports. Nous venons de voir que la répartition des ALU sur les ports d'émission est très variable d'un processeur à l'autre. Les divers choix possibles sont tous des compromis entre deux forces : réduire le nombre de ports d'émission d'un côté, garder de bonnes performances en réduisant la ''port contention'' de l'autre. Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction relativement simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ? Par contre, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal réparti les ALU. ===La séparation entre largeur de décodage et d'émission=== Sur les processeurs superscalaires larges, le processeur peut émettre plus d'instructions qu'il peut en décoder. Plus haut, j'ai dit que l'unité d'émission avait parfois plus de ports d'émission que de ports de décodage. La raison est assez simple, et elle s'explique assez bien avec un exemple. Prenons un processeur avec trois ALU entières, une FPU, une unité de branchement et une unité mémoire. Chacune a son propre port d'émission, ce qui fait que le CPU est capable d'émettre 6 µops. Du moins, c'est la théorie, car les conditions pour sont assez rares. Il faut qu'en chargeant un paquet de 6 instructions, il y ait exactement trois instructions entières, une flottante, un branchement et un accès mémoire. Si le paquet contient 5 instructions entières et un branchement, seules quatre instructions sont émises. Dans cet exemple, dans la grande majorité des cas, seuls 4 ports seront utilisés en moyenne et deux seront sous-utilisés. Il s'agit là d'une moyenne, qui dépend du code, et des instructions exactes à exécuter. Cependant, cette moyenne nous donne un moyen d'économiser beaucoup de circuits pour un cout en performance mineur. L'idée est de ne charger que des blocs de 4 instructions, mais d'avoir 6 ports d'émission. Le processeur charge et décode 4 instructions, les quatre instructions sont envoyées sur les ports d'émission adéquat. Le cout en transistors est alors situé entre celui d'un CPU quadruple émission et sextuple émission. Et les performances sont aussi entre les deux. Pour les performances, c'est parce que cela permet d'avoir un CPU quadruple émission avec bien moins de contraintes d’appariement. Imaginez qu'on se soit limité à quatre ports d'émission : il aurait fallu mettre plusieurs unités sur le même port d'émission. En conséquence, il y aurait eu des combinaisons interdites, comme émettre deux instructions entières + un branchement + un accès mémoire ; ou deux instructions entières + une opération flottante + un accès mémoire. De tells appariement interdits sont possibles en utilisant les six ports d'émission. ==L'émission multiple des accès mémoire== Les premiers processeurs superscalaires ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles. Par contre, c'est autre chose sur les processeurs superscalaires larges. Vu qu'ils travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares. L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. La ''load/store queue'' reste une structure unique, mais on peut lui rajouter des ports. Et à ce peit jeu, il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire. La première duplique les unités de calcul, mais pas les ports d'accès au cache. Les accès mémoire sont alors accumulés dans la ''load/store queue'', qui sont ensuite traités un par un par le cache. Un processeur utilisant cette technique est, par exemple, capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. Un exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. Ces processeurs pouvaient calculer trois adresses simultanément, mais ne pouvaient faire qu'un seul accès au cache par cycle. Le cout en circuit est modéré : les unités de calcul coutent très peu, les ports de la ''load/store queue'' coutent déjà un peu plus. La seconde méthode ajoute des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultanées. Il faut alors dupliquer des unités de calcul, rajouter les ports sur le cache, mais aussi rajouter des ports sur la ''load/store queue'', pour qu'elle échange des données/commandes mémoire avec le cache. Le cout en circuits est alors conséquent Quelques processeurs utilisaient des solutions intermédiaires, un exemple classique étant les Pentium 1 et 2 d'Intel. Mais nous en parlerons dans le chapitre suivant. [[File:Processeur non-superscalaire basique 02.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]] Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre. Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Le Pentium 4 était dans ce cas, mais il était un peu seul dans sa catégorie. Un cas similaire est celui où l'unité mémoire est alimentée par une fenêtre de micro-opération mélangeant instructions mémoire et entières. Les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. Dans ce cas, l'émission multiple est rarement implémentée. Le cas suivant a une file pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique. Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui pouvaient seulement émettre une lecture en même temps qu'une écriture. Ils avaient deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse. [[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]] Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture demande d'utiliser une file d'écriture multiport, ce qui est assez compliqué. Implémenter des lectures en plus est plus simple : il suffit de rajouter une seconde file de µops LOAD et une unité de calcul, alimentées via un second port d'émission. Il est aussi possible de rajouter un second port de lecture au cache, mais ce n'est pas obligatoire. Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse. De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports de lecture et d'écriture car il y a plus de lecture que d'écritures. Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. : Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse. [[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]] Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettent plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail. Pour résumer, l'évolution des CPU superscalaires a été la suivante : * pipeline mémorie fusionné avec le pipeline entier ; * séparation du pipeline mémoire du reste, avec émission parallèle des accès mémoire ; * émission multiple, mais sans dupliquer les ports du cache ; * émission multiple, avec émission simultanée d'une lecture et d’une écriture dans le cache ; * émission multiple avec plusieurs accès en lecture au cache. ==Les optimisations des processeurs superscalaires larges== Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul. ===L'étape de chargement d'un CPU superscalaire large=== Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 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 autoaligné.]] Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements ! [[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]] Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez 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 microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total). Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois 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 microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement. Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations. ===La micro-fusion des processeurs Intel et AMD=== La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''. Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''. Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout. La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Le parallélisme mémoire | prevText=Le parallélisme mémoire | next=Exemples de microarchitectures CPU : le cas du x86 | nextText=Exemples de microarchitectures CPU : le cas du x86 }} </noinclude> 2hhqvw1dro79pwu8qt8k28adhepf5aq 772692 772691 2026-09-21T14:56:58Z Mewtow 31375 /* Les CPU superscalaires à plusieurs unités spécialisées */ 772692 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 tout premiers processeurs superscalaires pouvaient émettre deux µops en même temps, ce qui portait le nom de '''double émission'''. Mais le nombre a rapidement augmenté et les processeurs modernes 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. ==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. Par exemple, l'unité de chargement charge deux fois plus d'instructions, mais n'est pas modifiée en profondeur. L'unité d'émission et les circuits d'exécution dans le désordre sont eux aussi altérés. Les unités de calcul sont souvent dupliquées, sauf sur les tout premiers CPU superscalaires. {|class="wikitable" |- ! rowspan="2" | Processeur sans émission multiple | rowspan="2" | Chargement | rowspan="2" | Décodage | rowspan="2" | Renommage | rowspan="2" | Émission | Exécution / ALU | rowspan="2" | ''Commit''/ROB |- | Exécution / ALU |- ! rowspan="4" | Processeur superscalaire | rowspan="4" | Chargement | rowspan="2" | Décodage | rowspan="4" | Renommage | rowspan="4" | Émission | Exécution / ALU | rowspan="4" | ''Commit''/ROB |- | Exécution / ALU |- | rowspan="2" | Décodage | Exécution / ALU |- | Exécution / ALU |} Voyons ce qu'il en est dans le détail. ===Les unités de calcul d'un processeur superscalaire=== Émettre N instructions simultanément demande implicitement d'exécuter N instructions par cycle, voire plus si des instructions multicycles viennent mettre leur grain de sel. Intuitivement, on se dit que les unités de calcul doivent être dupliquées. Et c'est une solution viable pour les processeurs très simples, avec seulement quelques ALU entières. Par exemple, un processeur RISC basique avec seulement une ALU entière et un ''barrel shifter'' peut être rendu superscalaire en dupliquant les deux circuits. Le problème, est que cette solution marche assez mal dès que le processeur contient des circuits multiplieurs et/ou une FPU. Par exemple, un processeur à double émission devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, etc. 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 ! Par contre, l'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission. Un autre cas extrême est celui où aucune unité de calcul n'est dupliquée. Pour comprendre comment c'est possible rappelez-vous qu'un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et éventuellement une unité de branchement. Cela suffit pour créer un processeur quadruple émission, très limité. Un tel processeur peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire. Le problème est qu'il est rare que l'on ait à exécuter en même temps un calcul entier, un calcul flottant, un accès mémoire et un branchement. La quadruple émission n'est donc que rarement utilisée. En pratique, les seules combinaisons fréquentes sont les combinaisons calcul + accès mémoire et calcul + branchement. Les combinaisons calcul entier + calcul flottant sont aussi possibles, mais plus rares. Mais on dépasse rarement la double émission. En clair, à partir de la quadruple émission, on ne peut plus se contenter d'une ALU, d'une FPU, d'une unité de branchement et d'une unité mémoire. Les processeurs superscalaires larges n'ont pas le choix que de rajouter des unités de calcul. Reste à voir ce qui est dupliqué en pratique. 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. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant. La conséquence est que les processeurs superscalaires ont des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a des paires d'instructions autorisées et des paires interdites. Par exemple, l'exécution simultanée de deux branchements est interdite, les branchements sont exécutés l'un après l'autre. Mais il est possible d'émettre un branchement en même temps qu'une autre instruction, en espérant que la prédiction de branchement ait fait une bonne prédiction. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur. En pratique, ces contraintes d’appariement ont peu d’impact. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière, le ratio est le même quand on remplace la multiplication par un branchement ou un accès mémoire. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Deux branchements consécutifs sont quant à eux plus fréquents, idem pour deux accès mémoire. Dupliquer des ALU entières est donc la priorité, suivi par la duplication des unités de branchement, puis celle des accès mémoire, pour terminer par le multiplieur et la FPU. ===Le chargement de plusieurs instructions consécutives=== Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large. Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence. Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas. Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement non-pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc. [[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]] ===Le décodage parallèle des instructions=== Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits. Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur. Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! ===L'unité d'émission d'un processeur superscalaire=== La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. Nous verrons pourquoi dans la suite, mais on peut dire que c'est juste plus simple et plus efficace ainsi. Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop candidate, sous entendu "candidate à l'émission". L'unité d'émission détecte les dépendances entre la µop candidate et les µops en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en cours d'exécution dans le pipeline. mais en plus, elle doit tester les dépendances entre les µops candidates. Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément, il doit détecter les paires d'instructions dépendantes et les sérialiser. Avec une fenêtre d'instruction, il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil gérent les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires modernes utilisent tous une fenêtre d'instruction centralisée ou décentralisée, et non des ''scoreboard''. Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps, là encore en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire peut émettre 8 µops à la fois, on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle. ===L'unité de renommage superscalaire=== Avec la superscalarité, l'unité de renommage de registre doit gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance de registre, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées. Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat. [[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]] Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante. [[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]] Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande. [[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]] ===Le banc de registre d'un processeur superscalaire=== Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement. 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. ==L'évolution historique des processeurs superscalaires== Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs triple émission permettent eux d’émettre trois instructions à la fois. Les tout premiers processeurs superscalaires avaient un budget en transistors très limité, ce qui fait qu'ils ne pouvaient pas être "totalement superscalaires". Par exemple, un processeur double émission ne pouvait pas émettre deux opérations entières en même temps, ni deux opérations flottantes, ni une opération entière avec un accès mémoire. Il y avait des paires d'instructions autorisées et des paires interdites. Les '''contraintes d'appariement''' sur les instructions à émettre en même temps étaient très strictes. L'intérêt de ces contraintes d’appariement est que le processeur était très simple, il n'y avait pas besoin de rajouter des circuits complexes pour rendre le processeur superscalaire. Les CPU superscalaires historiques ne faisaient que mieux utiliser des unités de calcul déjà présentes. Prenons un processeur non-superscalaire simple, illustré ci-dessous. Il contient : une unité de chargement, un décodeur, une unité d'émission, une ALU, une FPU, les bancs de registre entier et flottants, et une unité mémoire. Nous avons placé l'unité mémoire à la suite de l'ALU entière, car ce sera important pour les explications qui vont suivre. [[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]] Les processeurs superscalaires historiques modifiaient assez peu le schéma précédent. L'unité de chargement était adaptée pour lire plusieurs instructions depuis la mémoire, il y avait un seul décodeur, l'unité d'émission était assez peu modifiée. Mais une chose était certaine : aucune unité de calcul n'était dupliquée, le banc de registre n'était presque pas modifié. A la rigueur, gérer la triple 'émission avec les branchements demandait parfois d'ajouter des ports de lecture sur le banc de registre généraux, pas plus. ===La double émission entière-flottante=== Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est d'émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Le processeur a donc deux pipelines séparés : un pour les micro-opérations entières, un autre pour les micro-opérations flottantes. On parle alors de '''double émission entière-flottante'''. {|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, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité. La plupart des premiers processeurs superscalaires étaient de ce type. Les exemples les plus notables sont les processeurs POWER 1 et ses dérivés comme le ''RISC Single Chip''. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle. [[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]] La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisations visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate]. ===L'émission parallèle des branchements=== Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur. 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 modifier le banc de registres. Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. [[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]] Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications et les divisions ? Pour étudier ces questions, nous allons prendre le cas d'un processeur capable d’émettre trois opérations en même temps : deux opérations entières, et une flottante, potentiellement d'autres. {|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 |} 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|Emission multiple des opérations entières, implémentation courante]] Pour les branchements, deux solutions sont possibles. Avec la première, il y a émission parallèle des branchements, à savoir qu'il est possible d'émettre en branchement en parallèle de deux-trois instructions entières. Et cela impose la présence d'une unité de branchement séparée, avec son port d'émission. Avec la seconde, les branchements sont traités comme des instructions entières. Soit les branchements sont exécutés dans une ALU entière, soit il y a une unité de branchement reliée à un port d'émission pour les opérations entières. [[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Emission série et parallèle des branchements]] Avec la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales, sur les processeurs superscalaires historiques. En clair, le processeur peut émettre 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. De plus, 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. Ce compromis change cependant quand on passe à la quadruple émission. {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 |- | * ALU entière * Multiplieur | * ALU entière * Barrel shifter * Unité de branchement * Unité mémoire | FPU |- | * Additions, soustractions, opérations bit à bit, etc. * Multiplication | * Additions, soustractions, opérations bit à bit, etc. * Décalages et rotations * Branchements * Accès mémoire |Opération flottante |} ===Les CPU superscalaires à plusieurs unités spécialisées=== Pour la triple émission, deux opérations entières + une flottante est l'idéal. Même les processeurs modernes à triple émission restent sur cette "norme". Mais arrivé à la quadruple émission, les CPU superscalaires ont évolués dans des directions différentes, chacun faisant à sa sauce. La raison est que la quadruple émission permet de nombreuses possibilités, mais la plupart ne sont pas intéressantes. Prenons un CPU superscalaire avec une ALU entière, une FPU, une unité de branchement, et une unité mémoire. Il est possible de créer un CPU quadruple émission avec ces unités, mais le gain en performance n'en vaudrait pas la peine. Il est rare qu'un bloc de 4 instructions contienne exactement une opération entière, une flottante, un branchement et un accès mémoire. En pratique, les seules combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. En conséquence, une fois arrivé à la quadruple émission, les processeurs superscalaires n'ont pas le choix que de rajouter des unités de calcul, voire des unités mémoire. Et ils ont privilégié l'ajout d'ALU entières. La raison est que les programmes informatiques utilisent beaucoup plus d'additions/soustractions que le reste. Il y a environ 5 opérations entières de base pour une multiplication entière, le ratio est le même quand on remplace la multiplication par un branchement ou un accès mémoire. En conséquence, les ALU entières ont été dupliquées en premier, et dans de nombreux exemplaires. Reste à voir comment a été dupliqué le reste. Les multiplieurs n'ont pas été dupliqués, sauf sur les processeurs superscalaires très larges. La raison est qu'il est rare de devoir exécuter deux multiplications à la fois. Et c'est la même chose pour le ''barrel shifter''. Deux branchements consécutifs sont quant à eux plus fréquents, c'est même assez courant. Aussi, les unités de branchement ont été dupliquées en second lieu, après les ALU entières. Avoir plusieurs accès mémoire proches est assez courant, surtout sur les CPU de type CISC. Aussi, les unités mémoire ont été dupliquées en troisième lieu. Pour résumer, dupliquer des ALU entières est donc la priorité, suivi par la duplication des unités de branchement, puis celle des accès mémoire, pour terminer par le multiplieur et le ''barrel shifter''. La FPU est un peu à part. Une autre tendance a été de séparer l'unité mémoire des ALU entières, ce qui a permis l''''émission parallèle des µops mémoire'''. Comme son nom l'indique, l'idée est de séparer les µops mémoire des opérations entières. Il devient alors possible d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière. La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple. La '''double émission flottante''' a été utilisée sur les processeurs Alpha. L'idée est d'émettre deux opérations flottantes à la fois. Sauf qu'une implémentation naïve demanderait de dupliquer la FPU, ce qui aurait un cout en circuit rédhibitoire. Pour rappel, les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant. L'idée est d'avoir des ports d'émission séparés pour l'additionneur flottant et le multiplieur flottant. Le processeur peut alors émettre une addition flottante en même temps qu'une multiplication flottante. Les autres circuits de calcul flottant sont répartis sur ces deux ports d'émission flottants. L'avantage est que cela se marie bien avec l'usage d'une fenêtre d'instruction séparée pour les opérations flottantes. La fenêtre d'instruction a alors deux ports séparés, au lieu d'un seul. Rajouter un second port d'émission flottant n'est pas trop un problème, car le cout lié à l'ajout d'un port n'est pas linéaire. Passer de un port à deux a un cout tolérable, bien plus que de passer de 3 ports à 4 ou de 4 à 5. Il est possible de dupliquer l'additionneur et le multiplieur flottant, ce qui permet d'émettre deux additions et multiplications flottantes en même temps. C'est ce qui est fait sur les processeurs AMD d'architecture Zen 1 et 2. Ils ont deux additionneurs flottants par cœur, deux multiplieurs flottants, chacun avec leur propre port d'émission. Les performances en calcul flottant sont assez impressionnantes pour un processeur de l'époque. [[File:ZEN - émission multiple flottante.png|centre|vignette|upright=2|Microarchitecture Zen 1 d'AMD.]] ==Les ports d'émission et de décodage== Les ports d'émission et de décodage sont des ressources matérielles comme les autres. Dans cette section, nous allons parler de concepts liés aux ports d'émission et de décodage. Nous allons notamment parler du partage des ports d'émission, ainsi que des relations numériques entre ces deux types de ports. ===Le partage des ports d'émission et la ''port contention''=== Il est possible de partager un port d'émission, à savoir de connecter un port d'émission à plusieurs unités de calcul. Nous en avons vu un exemple plus haut, avec l'émission multiple des opérations entières. On peut par exemple connecter un multiplieur et une ALU entière sur le même port d'émission. Pour le moment, nous n'avons vu que des cas où un port d'émission est relié soit à des ALU entières, soit à des FPU, pas aux deux. Mais il existe des processeurs où un même port d'émission alimente à la fois une ALU entière et une FPU. Par exemple, on peut relier un additionneur flottant sur le même port qu'une ALU entière. Il faut noter que cela implique une fenêtre d'instruction centralisée, capable de mettre en attente micro-opérations entières et flottantes. Un exemple est celui des processeurs Core 2 Duo. Ils disposent de 6 ports d'émission, dont 3 ports dédiés à l'unité mémoire. Les 3 ports restants alimentent chacun : une ALU entière, un circuit de calcul flottant et une unité de calcul SSE (une unité de calcul SIMD, qu'on abordera dans quelques chapitres). * Le premier port alimente une ALU entière simple et un multiplieur/diviseur flottant. * Le second alimente une ALU entière, un multiplieur entier et un additionneur flottant. * Le troisième alimente une ALU entière, sans circuit flottant dédié. Dans le même genre, il est possible de partager un port d'émission entre l'unité mémoire et une ALU entière. Cela permet d'utiliser l'ALU entière pour les calculs d'adresse, ce qui évite d'avoir à utiliser une unité de calcul d'adresse distincte. Un exemple est celui du processeur superscalaire double émission Power PC 440. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. L'organisation en question permet soit d'émettre un accès mémoire en même temps qu'une opération entière, soit d'émettre deux opérations entières simples, soit d’émettre une multiplication et une addition/soustraction/comparaison. Une organisation simple, mais très efficace ! [[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]] Regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles, qui ne se présentent que pour certaines combinaisons de micro-opérations bien précises. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port. Le nombre de ports peut être un facteur limitant pour la performance dans certaines situations de '''''port contention''''' où on a assez d'ALU pour exécuter N micro-opérations, mais où la répartition des ALUs sur les ports l’empêche. Mais on peut réduire la ''port contention'' en répartissant correctement les ALU sur les ports d'émission. Le choix en question dépendent fortement du fait qu'en moyenne, que certaines paires d'instructions sont plus fréquentes que d'autres. Tout cela guide ce choix de répartition des ALUs sur les ports. Nous venons de voir que la répartition des ALU sur les ports d'émission est très variable d'un processeur à l'autre. Les divers choix possibles sont tous des compromis entre deux forces : réduire le nombre de ports d'émission d'un côté, garder de bonnes performances en réduisant la ''port contention'' de l'autre. Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction relativement simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ? Par contre, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal réparti les ALU. ===La séparation entre largeur de décodage et d'émission=== Sur les processeurs superscalaires larges, le processeur peut émettre plus d'instructions qu'il peut en décoder. Plus haut, j'ai dit que l'unité d'émission avait parfois plus de ports d'émission que de ports de décodage. La raison est assez simple, et elle s'explique assez bien avec un exemple. Prenons un processeur avec trois ALU entières, une FPU, une unité de branchement et une unité mémoire. Chacune a son propre port d'émission, ce qui fait que le CPU est capable d'émettre 6 µops. Du moins, c'est la théorie, car les conditions pour sont assez rares. Il faut qu'en chargeant un paquet de 6 instructions, il y ait exactement trois instructions entières, une flottante, un branchement et un accès mémoire. Si le paquet contient 5 instructions entières et un branchement, seules quatre instructions sont émises. Dans cet exemple, dans la grande majorité des cas, seuls 4 ports seront utilisés en moyenne et deux seront sous-utilisés. Il s'agit là d'une moyenne, qui dépend du code, et des instructions exactes à exécuter. Cependant, cette moyenne nous donne un moyen d'économiser beaucoup de circuits pour un cout en performance mineur. L'idée est de ne charger que des blocs de 4 instructions, mais d'avoir 6 ports d'émission. Le processeur charge et décode 4 instructions, les quatre instructions sont envoyées sur les ports d'émission adéquat. Le cout en transistors est alors situé entre celui d'un CPU quadruple émission et sextuple émission. Et les performances sont aussi entre les deux. Pour les performances, c'est parce que cela permet d'avoir un CPU quadruple émission avec bien moins de contraintes d’appariement. Imaginez qu'on se soit limité à quatre ports d'émission : il aurait fallu mettre plusieurs unités sur le même port d'émission. En conséquence, il y aurait eu des combinaisons interdites, comme émettre deux instructions entières + un branchement + un accès mémoire ; ou deux instructions entières + une opération flottante + un accès mémoire. De tells appariement interdits sont possibles en utilisant les six ports d'émission. ==L'émission multiple des accès mémoire== Les premiers processeurs superscalaires ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles. Par contre, c'est autre chose sur les processeurs superscalaires larges. Vu qu'ils travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares. L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. La ''load/store queue'' reste une structure unique, mais on peut lui rajouter des ports. Et à ce peit jeu, il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire. La première duplique les unités de calcul, mais pas les ports d'accès au cache. Les accès mémoire sont alors accumulés dans la ''load/store queue'', qui sont ensuite traités un par un par le cache. Un processeur utilisant cette technique est, par exemple, capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. Un exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. Ces processeurs pouvaient calculer trois adresses simultanément, mais ne pouvaient faire qu'un seul accès au cache par cycle. Le cout en circuit est modéré : les unités de calcul coutent très peu, les ports de la ''load/store queue'' coutent déjà un peu plus. La seconde méthode ajoute des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultanées. Il faut alors dupliquer des unités de calcul, rajouter les ports sur le cache, mais aussi rajouter des ports sur la ''load/store queue'', pour qu'elle échange des données/commandes mémoire avec le cache. Le cout en circuits est alors conséquent Quelques processeurs utilisaient des solutions intermédiaires, un exemple classique étant les Pentium 1 et 2 d'Intel. Mais nous en parlerons dans le chapitre suivant. [[File:Processeur non-superscalaire basique 02.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]] Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre. Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Le Pentium 4 était dans ce cas, mais il était un peu seul dans sa catégorie. Un cas similaire est celui où l'unité mémoire est alimentée par une fenêtre de micro-opération mélangeant instructions mémoire et entières. Les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. Dans ce cas, l'émission multiple est rarement implémentée. Le cas suivant a une file pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique. Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui pouvaient seulement émettre une lecture en même temps qu'une écriture. Ils avaient deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse. [[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]] Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture demande d'utiliser une file d'écriture multiport, ce qui est assez compliqué. Implémenter des lectures en plus est plus simple : il suffit de rajouter une seconde file de µops LOAD et une unité de calcul, alimentées via un second port d'émission. Il est aussi possible de rajouter un second port de lecture au cache, mais ce n'est pas obligatoire. Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse. De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports de lecture et d'écriture car il y a plus de lecture que d'écritures. Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. : Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse. [[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]] Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettent plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail. Pour résumer, l'évolution des CPU superscalaires a été la suivante : * pipeline mémorie fusionné avec le pipeline entier ; * séparation du pipeline mémoire du reste, avec émission parallèle des accès mémoire ; * émission multiple, mais sans dupliquer les ports du cache ; * émission multiple, avec émission simultanée d'une lecture et d’une écriture dans le cache ; * émission multiple avec plusieurs accès en lecture au cache. ==Les optimisations des processeurs superscalaires larges== Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul. ===L'étape de chargement d'un CPU superscalaire large=== Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 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 autoaligné.]] Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements ! [[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]] Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez 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 microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total). Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois 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 microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement. Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations. ===La micro-fusion des processeurs Intel et AMD=== La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''. Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''. Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout. La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Le parallélisme mémoire | prevText=Le parallélisme mémoire | next=Exemples de microarchitectures CPU : le cas du x86 | nextText=Exemples de microarchitectures CPU : le cas du x86 }} </noinclude> b0u6b3spon6wtynws4rxmtzns48a5cz 772693 772692 2026-09-21T14:57:37Z Mewtow 31375 /* Les CPU superscalaires à plusieurs unités spécialisées */ 772693 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 tout premiers processeurs superscalaires pouvaient émettre deux µops en même temps, ce qui portait le nom de '''double émission'''. Mais le nombre a rapidement augmenté et les processeurs modernes 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. ==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. Par exemple, l'unité de chargement charge deux fois plus d'instructions, mais n'est pas modifiée en profondeur. L'unité d'émission et les circuits d'exécution dans le désordre sont eux aussi altérés. Les unités de calcul sont souvent dupliquées, sauf sur les tout premiers CPU superscalaires. {|class="wikitable" |- ! rowspan="2" | Processeur sans émission multiple | rowspan="2" | Chargement | rowspan="2" | Décodage | rowspan="2" | Renommage | rowspan="2" | Émission | Exécution / ALU | rowspan="2" | ''Commit''/ROB |- | Exécution / ALU |- ! rowspan="4" | Processeur superscalaire | rowspan="4" | Chargement | rowspan="2" | Décodage | rowspan="4" | Renommage | rowspan="4" | Émission | Exécution / ALU | rowspan="4" | ''Commit''/ROB |- | Exécution / ALU |- | rowspan="2" | Décodage | Exécution / ALU |- | Exécution / ALU |} Voyons ce qu'il en est dans le détail. ===Les unités de calcul d'un processeur superscalaire=== Émettre N instructions simultanément demande implicitement d'exécuter N instructions par cycle, voire plus si des instructions multicycles viennent mettre leur grain de sel. Intuitivement, on se dit que les unités de calcul doivent être dupliquées. Et c'est une solution viable pour les processeurs très simples, avec seulement quelques ALU entières. Par exemple, un processeur RISC basique avec seulement une ALU entière et un ''barrel shifter'' peut être rendu superscalaire en dupliquant les deux circuits. Le problème, est que cette solution marche assez mal dès que le processeur contient des circuits multiplieurs et/ou une FPU. Par exemple, un processeur à double émission devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, etc. 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 ! Par contre, l'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission. Un autre cas extrême est celui où aucune unité de calcul n'est dupliquée. Pour comprendre comment c'est possible rappelez-vous qu'un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et éventuellement une unité de branchement. Cela suffit pour créer un processeur quadruple émission, très limité. Un tel processeur peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire. Le problème est qu'il est rare que l'on ait à exécuter en même temps un calcul entier, un calcul flottant, un accès mémoire et un branchement. La quadruple émission n'est donc que rarement utilisée. En pratique, les seules combinaisons fréquentes sont les combinaisons calcul + accès mémoire et calcul + branchement. Les combinaisons calcul entier + calcul flottant sont aussi possibles, mais plus rares. Mais on dépasse rarement la double émission. En clair, à partir de la quadruple émission, on ne peut plus se contenter d'une ALU, d'une FPU, d'une unité de branchement et d'une unité mémoire. Les processeurs superscalaires larges n'ont pas le choix que de rajouter des unités de calcul. Reste à voir ce qui est dupliqué en pratique. 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. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant. La conséquence est que les processeurs superscalaires ont des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a des paires d'instructions autorisées et des paires interdites. Par exemple, l'exécution simultanée de deux branchements est interdite, les branchements sont exécutés l'un après l'autre. Mais il est possible d'émettre un branchement en même temps qu'une autre instruction, en espérant que la prédiction de branchement ait fait une bonne prédiction. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur. En pratique, ces contraintes d’appariement ont peu d’impact. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière, le ratio est le même quand on remplace la multiplication par un branchement ou un accès mémoire. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Deux branchements consécutifs sont quant à eux plus fréquents, idem pour deux accès mémoire. Dupliquer des ALU entières est donc la priorité, suivi par la duplication des unités de branchement, puis celle des accès mémoire, pour terminer par le multiplieur et la FPU. ===Le chargement de plusieurs instructions consécutives=== Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large. Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence. Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas. Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement non-pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc. [[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]] ===Le décodage parallèle des instructions=== Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits. Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur. Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! ===L'unité d'émission d'un processeur superscalaire=== La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. Nous verrons pourquoi dans la suite, mais on peut dire que c'est juste plus simple et plus efficace ainsi. Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop candidate, sous entendu "candidate à l'émission". L'unité d'émission détecte les dépendances entre la µop candidate et les µops en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en cours d'exécution dans le pipeline. mais en plus, elle doit tester les dépendances entre les µops candidates. Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément, il doit détecter les paires d'instructions dépendantes et les sérialiser. Avec une fenêtre d'instruction, il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil gérent les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires modernes utilisent tous une fenêtre d'instruction centralisée ou décentralisée, et non des ''scoreboard''. Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps, là encore en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire peut émettre 8 µops à la fois, on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle. ===L'unité de renommage superscalaire=== Avec la superscalarité, l'unité de renommage de registre doit gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance de registre, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées. Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat. [[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]] Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante. [[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]] Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande. [[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]] ===Le banc de registre d'un processeur superscalaire=== Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement. 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. ==L'évolution historique des processeurs superscalaires== Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs triple émission permettent eux d’émettre trois instructions à la fois. Les tout premiers processeurs superscalaires avaient un budget en transistors très limité, ce qui fait qu'ils ne pouvaient pas être "totalement superscalaires". Par exemple, un processeur double émission ne pouvait pas émettre deux opérations entières en même temps, ni deux opérations flottantes, ni une opération entière avec un accès mémoire. Il y avait des paires d'instructions autorisées et des paires interdites. Les '''contraintes d'appariement''' sur les instructions à émettre en même temps étaient très strictes. L'intérêt de ces contraintes d’appariement est que le processeur était très simple, il n'y avait pas besoin de rajouter des circuits complexes pour rendre le processeur superscalaire. Les CPU superscalaires historiques ne faisaient que mieux utiliser des unités de calcul déjà présentes. Prenons un processeur non-superscalaire simple, illustré ci-dessous. Il contient : une unité de chargement, un décodeur, une unité d'émission, une ALU, une FPU, les bancs de registre entier et flottants, et une unité mémoire. Nous avons placé l'unité mémoire à la suite de l'ALU entière, car ce sera important pour les explications qui vont suivre. [[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]] Les processeurs superscalaires historiques modifiaient assez peu le schéma précédent. L'unité de chargement était adaptée pour lire plusieurs instructions depuis la mémoire, il y avait un seul décodeur, l'unité d'émission était assez peu modifiée. Mais une chose était certaine : aucune unité de calcul n'était dupliquée, le banc de registre n'était presque pas modifié. A la rigueur, gérer la triple 'émission avec les branchements demandait parfois d'ajouter des ports de lecture sur le banc de registre généraux, pas plus. ===La double émission entière-flottante=== Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est d'émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Le processeur a donc deux pipelines séparés : un pour les micro-opérations entières, un autre pour les micro-opérations flottantes. On parle alors de '''double émission entière-flottante'''. {|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, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité. La plupart des premiers processeurs superscalaires étaient de ce type. Les exemples les plus notables sont les processeurs POWER 1 et ses dérivés comme le ''RISC Single Chip''. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle. [[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]] La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisations visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate]. ===L'émission parallèle des branchements=== Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur. 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 modifier le banc de registres. Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. [[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]] Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications et les divisions ? Pour étudier ces questions, nous allons prendre le cas d'un processeur capable d’émettre trois opérations en même temps : deux opérations entières, et une flottante, potentiellement d'autres. {|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 |} 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|Emission multiple des opérations entières, implémentation courante]] Pour les branchements, deux solutions sont possibles. Avec la première, il y a émission parallèle des branchements, à savoir qu'il est possible d'émettre en branchement en parallèle de deux-trois instructions entières. Et cela impose la présence d'une unité de branchement séparée, avec son port d'émission. Avec la seconde, les branchements sont traités comme des instructions entières. Soit les branchements sont exécutés dans une ALU entière, soit il y a une unité de branchement reliée à un port d'émission pour les opérations entières. [[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Emission série et parallèle des branchements]] Avec la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales, sur les processeurs superscalaires historiques. En clair, le processeur peut émettre 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. De plus, 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. Ce compromis change cependant quand on passe à la quadruple émission. {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 |- | * ALU entière * Multiplieur | * ALU entière * Barrel shifter * Unité de branchement * Unité mémoire | FPU |- | * Additions, soustractions, opérations bit à bit, etc. * Multiplication | * Additions, soustractions, opérations bit à bit, etc. * Décalages et rotations * Branchements * Accès mémoire |Opération flottante |} ===Les CPU superscalaires à plusieurs unités spécialisées=== Pour la triple émission, deux opérations entières + une flottante est l'idéal. Même les processeurs modernes à triple émission restent sur cette "norme". Mais arrivé à la quadruple émission, les CPU superscalaires ont évolués dans des directions différentes, chacun faisant à sa sauce. La raison est que la quadruple émission permet de nombreuses possibilités, mais la plupart ne sont pas intéressantes. Prenons un CPU superscalaire avec une ALU entière, une FPU, une unité de branchement, et une unité mémoire. Il est possible de créer un CPU quadruple émission avec ces unités, mais le gain en performance n'en vaudrait pas la peine. Il est rare qu'un bloc de 4 instructions contienne exactement une opération entière, une flottante, un branchement et un accès mémoire. En pratique, les seules combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. En conséquence, une fois arrivé à la quadruple émission, les processeurs superscalaires n'ont pas le choix que de rajouter des unités de calcul, voire des unités mémoire. Et ils ont privilégié l'ajout d'ALU entières. La raison est que les programmes informatiques utilisent beaucoup plus d'additions/soustractions que le reste. Il y a environ 5 opérations entières de base pour une multiplication entière, le ratio est le même quand on remplace la multiplication par un branchement ou un accès mémoire. En conséquence, les ALU entières ont été dupliquées en premier, et dans de nombreux exemplaires. Reste à voir comment a été dupliqué le reste. Les multiplieurs n'ont pas été dupliqués, sauf sur les processeurs superscalaires très larges. La raison est qu'il est rare de devoir exécuter deux multiplications à la fois. Et c'est la même chose pour le ''barrel shifter''. Deux branchements consécutifs sont quant à eux plus fréquents, c'est même assez courant. Aussi, les unités de branchement ont été dupliquées en second lieu, après les ALU entières. Avoir plusieurs accès mémoire proches est assez courant, surtout sur les CPU de type CISC. Aussi, les unités mémoire ont été dupliquées en troisième lieu. Pour résumer, dupliquer des ALU entières est donc la priorité, suivi par la duplication des unités de branchement, puis celle des accès mémoire, pour terminer par le multiplieur et le ''barrel shifter''. La FPU est un peu à part. Une autre tendance a été de séparer l'unité mémoire des ALU entières, ce qui a permis l''''émission parallèle des µops mémoire'''. Comme son nom l'indique, l'idée est d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière. La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple. La '''double émission flottante''' a été utilisée sur les processeurs Alpha. L'idée est d'émettre deux opérations flottantes à la fois. Sauf qu'une implémentation naïve demanderait de dupliquer la FPU, ce qui aurait un cout en circuit rédhibitoire. Pour rappel, les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant. L'idée est d'avoir des ports d'émission séparés pour l'additionneur flottant et le multiplieur flottant. Le processeur peut alors émettre une addition flottante en même temps qu'une multiplication flottante. Les autres circuits de calcul flottant sont répartis sur ces deux ports d'émission flottants. L'avantage est que cela se marie bien avec l'usage d'une fenêtre d'instruction séparée pour les opérations flottantes. La fenêtre d'instruction a alors deux ports séparés, au lieu d'un seul. Rajouter un second port d'émission flottant n'est pas trop un problème, car le cout lié à l'ajout d'un port n'est pas linéaire. Passer de un port à deux a un cout tolérable, bien plus que de passer de 3 ports à 4 ou de 4 à 5. Il est possible de dupliquer l'additionneur et le multiplieur flottant, ce qui permet d'émettre deux additions et multiplications flottantes en même temps. C'est ce qui est fait sur les processeurs AMD d'architecture Zen 1 et 2. Ils ont deux additionneurs flottants par cœur, deux multiplieurs flottants, chacun avec leur propre port d'émission. Les performances en calcul flottant sont assez impressionnantes pour un processeur de l'époque. [[File:ZEN - émission multiple flottante.png|centre|vignette|upright=2|Microarchitecture Zen 1 d'AMD.]] ==Les ports d'émission et de décodage== Les ports d'émission et de décodage sont des ressources matérielles comme les autres. Dans cette section, nous allons parler de concepts liés aux ports d'émission et de décodage. Nous allons notamment parler du partage des ports d'émission, ainsi que des relations numériques entre ces deux types de ports. ===Le partage des ports d'émission et la ''port contention''=== Il est possible de partager un port d'émission, à savoir de connecter un port d'émission à plusieurs unités de calcul. Nous en avons vu un exemple plus haut, avec l'émission multiple des opérations entières. On peut par exemple connecter un multiplieur et une ALU entière sur le même port d'émission. Pour le moment, nous n'avons vu que des cas où un port d'émission est relié soit à des ALU entières, soit à des FPU, pas aux deux. Mais il existe des processeurs où un même port d'émission alimente à la fois une ALU entière et une FPU. Par exemple, on peut relier un additionneur flottant sur le même port qu'une ALU entière. Il faut noter que cela implique une fenêtre d'instruction centralisée, capable de mettre en attente micro-opérations entières et flottantes. Un exemple est celui des processeurs Core 2 Duo. Ils disposent de 6 ports d'émission, dont 3 ports dédiés à l'unité mémoire. Les 3 ports restants alimentent chacun : une ALU entière, un circuit de calcul flottant et une unité de calcul SSE (une unité de calcul SIMD, qu'on abordera dans quelques chapitres). * Le premier port alimente une ALU entière simple et un multiplieur/diviseur flottant. * Le second alimente une ALU entière, un multiplieur entier et un additionneur flottant. * Le troisième alimente une ALU entière, sans circuit flottant dédié. Dans le même genre, il est possible de partager un port d'émission entre l'unité mémoire et une ALU entière. Cela permet d'utiliser l'ALU entière pour les calculs d'adresse, ce qui évite d'avoir à utiliser une unité de calcul d'adresse distincte. Un exemple est celui du processeur superscalaire double émission Power PC 440. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. L'organisation en question permet soit d'émettre un accès mémoire en même temps qu'une opération entière, soit d'émettre deux opérations entières simples, soit d’émettre une multiplication et une addition/soustraction/comparaison. Une organisation simple, mais très efficace ! [[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]] Regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles, qui ne se présentent que pour certaines combinaisons de micro-opérations bien précises. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port. Le nombre de ports peut être un facteur limitant pour la performance dans certaines situations de '''''port contention''''' où on a assez d'ALU pour exécuter N micro-opérations, mais où la répartition des ALUs sur les ports l’empêche. Mais on peut réduire la ''port contention'' en répartissant correctement les ALU sur les ports d'émission. Le choix en question dépendent fortement du fait qu'en moyenne, que certaines paires d'instructions sont plus fréquentes que d'autres. Tout cela guide ce choix de répartition des ALUs sur les ports. Nous venons de voir que la répartition des ALU sur les ports d'émission est très variable d'un processeur à l'autre. Les divers choix possibles sont tous des compromis entre deux forces : réduire le nombre de ports d'émission d'un côté, garder de bonnes performances en réduisant la ''port contention'' de l'autre. Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction relativement simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ? Par contre, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal réparti les ALU. ===La séparation entre largeur de décodage et d'émission=== Sur les processeurs superscalaires larges, le processeur peut émettre plus d'instructions qu'il peut en décoder. Plus haut, j'ai dit que l'unité d'émission avait parfois plus de ports d'émission que de ports de décodage. La raison est assez simple, et elle s'explique assez bien avec un exemple. Prenons un processeur avec trois ALU entières, une FPU, une unité de branchement et une unité mémoire. Chacune a son propre port d'émission, ce qui fait que le CPU est capable d'émettre 6 µops. Du moins, c'est la théorie, car les conditions pour sont assez rares. Il faut qu'en chargeant un paquet de 6 instructions, il y ait exactement trois instructions entières, une flottante, un branchement et un accès mémoire. Si le paquet contient 5 instructions entières et un branchement, seules quatre instructions sont émises. Dans cet exemple, dans la grande majorité des cas, seuls 4 ports seront utilisés en moyenne et deux seront sous-utilisés. Il s'agit là d'une moyenne, qui dépend du code, et des instructions exactes à exécuter. Cependant, cette moyenne nous donne un moyen d'économiser beaucoup de circuits pour un cout en performance mineur. L'idée est de ne charger que des blocs de 4 instructions, mais d'avoir 6 ports d'émission. Le processeur charge et décode 4 instructions, les quatre instructions sont envoyées sur les ports d'émission adéquat. Le cout en transistors est alors situé entre celui d'un CPU quadruple émission et sextuple émission. Et les performances sont aussi entre les deux. Pour les performances, c'est parce que cela permet d'avoir un CPU quadruple émission avec bien moins de contraintes d’appariement. Imaginez qu'on se soit limité à quatre ports d'émission : il aurait fallu mettre plusieurs unités sur le même port d'émission. En conséquence, il y aurait eu des combinaisons interdites, comme émettre deux instructions entières + un branchement + un accès mémoire ; ou deux instructions entières + une opération flottante + un accès mémoire. De tells appariement interdits sont possibles en utilisant les six ports d'émission. ==L'émission multiple des accès mémoire== Les premiers processeurs superscalaires ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles. Par contre, c'est autre chose sur les processeurs superscalaires larges. Vu qu'ils travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares. L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. La ''load/store queue'' reste une structure unique, mais on peut lui rajouter des ports. Et à ce peit jeu, il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire. La première duplique les unités de calcul, mais pas les ports d'accès au cache. Les accès mémoire sont alors accumulés dans la ''load/store queue'', qui sont ensuite traités un par un par le cache. Un processeur utilisant cette technique est, par exemple, capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. Un exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. Ces processeurs pouvaient calculer trois adresses simultanément, mais ne pouvaient faire qu'un seul accès au cache par cycle. Le cout en circuit est modéré : les unités de calcul coutent très peu, les ports de la ''load/store queue'' coutent déjà un peu plus. La seconde méthode ajoute des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultanées. Il faut alors dupliquer des unités de calcul, rajouter les ports sur le cache, mais aussi rajouter des ports sur la ''load/store queue'', pour qu'elle échange des données/commandes mémoire avec le cache. Le cout en circuits est alors conséquent Quelques processeurs utilisaient des solutions intermédiaires, un exemple classique étant les Pentium 1 et 2 d'Intel. Mais nous en parlerons dans le chapitre suivant. [[File:Processeur non-superscalaire basique 02.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]] Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre. Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Le Pentium 4 était dans ce cas, mais il était un peu seul dans sa catégorie. Un cas similaire est celui où l'unité mémoire est alimentée par une fenêtre de micro-opération mélangeant instructions mémoire et entières. Les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. Dans ce cas, l'émission multiple est rarement implémentée. Le cas suivant a une file pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique. Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui pouvaient seulement émettre une lecture en même temps qu'une écriture. Ils avaient deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse. [[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]] Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture demande d'utiliser une file d'écriture multiport, ce qui est assez compliqué. Implémenter des lectures en plus est plus simple : il suffit de rajouter une seconde file de µops LOAD et une unité de calcul, alimentées via un second port d'émission. Il est aussi possible de rajouter un second port de lecture au cache, mais ce n'est pas obligatoire. Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse. De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports de lecture et d'écriture car il y a plus de lecture que d'écritures. Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. : Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse. [[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]] Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettent plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail. Pour résumer, l'évolution des CPU superscalaires a été la suivante : * pipeline mémorie fusionné avec le pipeline entier ; * séparation du pipeline mémoire du reste, avec émission parallèle des accès mémoire ; * émission multiple, mais sans dupliquer les ports du cache ; * émission multiple, avec émission simultanée d'une lecture et d’une écriture dans le cache ; * émission multiple avec plusieurs accès en lecture au cache. ==Les optimisations des processeurs superscalaires larges== Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul. ===L'étape de chargement d'un CPU superscalaire large=== Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 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 autoaligné.]] Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements ! [[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]] Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez 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 microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total). Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois 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 microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement. Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations. ===La micro-fusion des processeurs Intel et AMD=== La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''. Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''. Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout. La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Le parallélisme mémoire | prevText=Le parallélisme mémoire | next=Exemples de microarchitectures CPU : le cas du x86 | nextText=Exemples de microarchitectures CPU : le cas du x86 }} </noinclude> sfti83fwnuqitl3g877rv02bbwmgrzh 772694 772693 2026-09-21T16:39:22Z Mewtow 31375 /* L'implémentation des processeurs superscalaires */ 772694 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 tout premiers processeurs superscalaires pouvaient émettre deux µops en même temps, ce qui portait le nom de '''double émission'''. Mais le nombre a rapidement augmenté et les processeurs modernes 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 des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. Il y a donc des paires d'instructions autorisées et des paires interdites. En pratique, ces contraintes d’appariement ont peu d’impact. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière, le ratio est le même quand on remplace la multiplication par un branchement ou un accès mémoire. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Deux branchements consécutifs sont quant à eux plus fréquents, idem pour deux accès mémoire. Dupliquer des ALU entières est donc la priorité, suivi par la duplication des unités de branchement, puis celle des accès mémoire, pour terminer par le multiplieur et la FPU. ==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. Par exemple, l'unité de chargement charge deux fois plus d'instructions, mais n'est pas modifiée en profondeur. L'unité d'émission et les circuits d'exécution dans le désordre sont eux aussi altérés. Les unités de calcul sont souvent dupliquées, sauf sur les tout premiers CPU superscalaires. {|class="wikitable" |- ! rowspan="2" | Processeur sans émission multiple | rowspan="2" | Chargement | rowspan="2" | Décodage | rowspan="2" | Renommage | rowspan="2" | Émission | Exécution / ALU | rowspan="2" | ''Commit''/ROB |- | Exécution / ALU |- ! rowspan="4" | Processeur superscalaire | rowspan="4" | Chargement | rowspan="2" | Décodage | rowspan="4" | Renommage | rowspan="4" | Émission | Exécution / ALU | rowspan="4" | ''Commit''/ROB |- | Exécution / ALU |- | rowspan="2" | Décodage | Exécution / ALU |- | Exécution / ALU |} Voyons ce qu'il en est dans le détail. ===Les unités de calcul d'un processeur superscalaire=== Émettre N instructions simultanément demande implicitement d'exécuter N instructions par cycle, voire plus si des instructions multicycles viennent mettre leur grain de sel. Intuitivement, on se dit que les unités de calcul doivent être dupliquées. Et c'est une solution viable pour les processeurs très simples, avec seulement quelques ALU entières. Par exemple, un processeur RISC basique avec seulement une ALU entière et un ''barrel shifter'' peut être rendu superscalaire en dupliquant les deux circuits. Le problème, est que cette solution marche assez mal dès que le processeur contient des circuits multiplieurs et/ou une FPU. Par exemple, un processeur à double émission devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, etc. 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 ! Par contre, l'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission. Un autre cas extrême est celui où aucune unité de calcul n'est dupliquée. Pour comprendre comment c'est possible rappelez-vous qu'un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et éventuellement une unité de branchement. Cela suffit pour créer un processeur quadruple émission, très limité. Un tel processeur peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire. Le problème est qu'il est rare que l'on ait à exécuter en même temps un calcul entier, un calcul flottant, un accès mémoire et un branchement. La quadruple émission n'est donc que rarement utilisée. En pratique, les seules combinaisons fréquentes sont les combinaisons calcul + accès mémoire et calcul + branchement. Les combinaisons calcul entier + calcul flottant sont aussi possibles, mais plus rares. Mais on dépasse rarement la double émission. En clair, à partir de la quadruple émission, on ne peut plus se contenter d'une ALU, d'une FPU, d'une unité de branchement et d'une unité mémoire. Les processeurs superscalaires larges n'ont pas le choix que de rajouter des unités de calcul. Reste à voir ce qui est dupliqué en pratique. 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. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant. ===Le chargement de plusieurs instructions consécutives=== Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large. Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence. Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas. Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement non-pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc. [[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]] ===Le décodage parallèle des instructions=== Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits. Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur. Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! ===L'unité d'émission d'un processeur superscalaire=== La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. Nous verrons pourquoi dans la suite, mais on peut dire que c'est juste plus simple et plus efficace ainsi. Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop candidate, sous entendu "candidate à l'émission". L'unité d'émission détecte les dépendances entre la µop candidate et les µops en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en cours d'exécution dans le pipeline. mais en plus, elle doit tester les dépendances entre les µops candidates. Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément, il doit détecter les paires d'instructions dépendantes et les sérialiser. Avec une fenêtre d'instruction, il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil gérent les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires modernes utilisent tous une fenêtre d'instruction centralisée ou décentralisée, et non des ''scoreboard''. Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps, là encore en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire peut émettre 8 µops à la fois, on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle. ===L'unité de renommage superscalaire=== Avec la superscalarité, l'unité de renommage de registre doit gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance de registre, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées. Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat. [[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]] Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante. [[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]] Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande. [[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]] ===Le banc de registre d'un processeur superscalaire=== Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement. 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. ==L'évolution historique des processeurs superscalaires== Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs triple émission permettent eux d’émettre trois instructions à la fois. Les tout premiers processeurs superscalaires avaient un budget en transistors très limité, ce qui fait qu'ils ne pouvaient pas être "totalement superscalaires". Par exemple, un processeur double émission ne pouvait pas émettre deux opérations entières en même temps, ni deux opérations flottantes, ni une opération entière avec un accès mémoire. Il y avait des paires d'instructions autorisées et des paires interdites. Les '''contraintes d'appariement''' sur les instructions à émettre en même temps étaient très strictes. L'intérêt de ces contraintes d’appariement est que le processeur était très simple, il n'y avait pas besoin de rajouter des circuits complexes pour rendre le processeur superscalaire. Les CPU superscalaires historiques ne faisaient que mieux utiliser des unités de calcul déjà présentes. Prenons un processeur non-superscalaire simple, illustré ci-dessous. Il contient : une unité de chargement, un décodeur, une unité d'émission, une ALU, une FPU, les bancs de registre entier et flottants, et une unité mémoire. Nous avons placé l'unité mémoire à la suite de l'ALU entière, car ce sera important pour les explications qui vont suivre. [[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]] Les processeurs superscalaires historiques modifiaient assez peu le schéma précédent. L'unité de chargement était adaptée pour lire plusieurs instructions depuis la mémoire, il y avait un seul décodeur, l'unité d'émission était assez peu modifiée. Mais une chose était certaine : aucune unité de calcul n'était dupliquée, le banc de registre n'était presque pas modifié. A la rigueur, gérer la triple 'émission avec les branchements demandait parfois d'ajouter des ports de lecture sur le banc de registre généraux, pas plus. ===La double émission entière-flottante=== Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est d'émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Le processeur a donc deux pipelines séparés : un pour les micro-opérations entières, un autre pour les micro-opérations flottantes. On parle alors de '''double émission entière-flottante'''. {|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, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité. La plupart des premiers processeurs superscalaires étaient de ce type. Les exemples les plus notables sont les processeurs POWER 1 et ses dérivés comme le ''RISC Single Chip''. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle. [[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]] La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisations visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate]. ===L'émission parallèle des branchements=== Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur. 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 modifier le banc de registres. Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. [[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]] Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications et les divisions ? Pour étudier ces questions, nous allons prendre le cas d'un processeur capable d’émettre trois opérations en même temps : deux opérations entières, et une flottante, potentiellement d'autres. {|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 |} 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|Emission multiple des opérations entières, implémentation courante]] Pour les branchements, deux solutions sont possibles. Avec la première, il y a émission parallèle des branchements, à savoir qu'il est possible d'émettre en branchement en parallèle de deux-trois instructions entières. Et cela impose la présence d'une unité de branchement séparée, avec son port d'émission. Avec la seconde, les branchements sont traités comme des instructions entières. Soit les branchements sont exécutés dans une ALU entière, soit il y a une unité de branchement reliée à un port d'émission pour les opérations entières. [[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Emission série et parallèle des branchements]] Avec la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales, sur les processeurs superscalaires historiques. En clair, le processeur peut émettre 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. De plus, 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. Ce compromis change cependant quand on passe à la quadruple émission. {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 |- | * ALU entière * Multiplieur | * ALU entière * Barrel shifter * Unité de branchement * Unité mémoire | FPU |- | * Additions, soustractions, opérations bit à bit, etc. * Multiplication | * Additions, soustractions, opérations bit à bit, etc. * Décalages et rotations * Branchements * Accès mémoire |Opération flottante |} ===Les CPU superscalaires à plusieurs unités spécialisées=== Pour la triple émission, deux opérations entières + une flottante est l'idéal. Même les processeurs modernes à triple émission restent sur cette "norme". Mais arrivé à la quadruple émission, les CPU superscalaires ont évolués dans des directions différentes, chacun faisant à sa sauce. La raison est que la quadruple émission permet de nombreuses possibilités, mais la plupart ne sont pas intéressantes. Prenons un CPU superscalaire avec une ALU entière, une FPU, une unité de branchement, et une unité mémoire. Il est possible de créer un CPU quadruple émission avec ces unités, mais le gain en performance n'en vaudrait pas la peine. Il est rare qu'un bloc de 4 instructions contienne exactement une opération entière, une flottante, un branchement et un accès mémoire. En pratique, les seules combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. En conséquence, une fois arrivé à la quadruple émission, les processeurs superscalaires n'ont pas le choix que de rajouter des unités de calcul, voire des unités mémoire. Et ils ont privilégié l'ajout d'ALU entières. La raison est que les programmes informatiques utilisent beaucoup plus d'additions/soustractions que le reste. Il y a environ 5 opérations entières de base pour une multiplication entière, le ratio est le même quand on remplace la multiplication par un branchement ou un accès mémoire. En conséquence, les ALU entières ont été dupliquées en premier, et dans de nombreux exemplaires. Reste à voir comment a été dupliqué le reste. Les multiplieurs n'ont pas été dupliqués, sauf sur les processeurs superscalaires très larges. La raison est qu'il est rare de devoir exécuter deux multiplications à la fois. Et c'est la même chose pour le ''barrel shifter''. Deux branchements consécutifs sont quant à eux plus fréquents, c'est même assez courant. Aussi, les unités de branchement ont été dupliquées en second lieu, après les ALU entières. Avoir plusieurs accès mémoire proches est assez courant, surtout sur les CPU de type CISC. Aussi, les unités mémoire ont été dupliquées en troisième lieu. Pour résumer, dupliquer des ALU entières est donc la priorité, suivi par la duplication des unités de branchement, puis celle des accès mémoire, pour terminer par le multiplieur et le ''barrel shifter''. La FPU est un peu à part. Une autre tendance a été de séparer l'unité mémoire des ALU entières, ce qui a permis l''''émission parallèle des µops mémoire'''. Comme son nom l'indique, l'idée est d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière. La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple. La '''double émission flottante''' a été utilisée sur les processeurs Alpha. L'idée est d'émettre deux opérations flottantes à la fois. Sauf qu'une implémentation naïve demanderait de dupliquer la FPU, ce qui aurait un cout en circuit rédhibitoire. Pour rappel, les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant. L'idée est d'avoir des ports d'émission séparés pour l'additionneur flottant et le multiplieur flottant. Le processeur peut alors émettre une addition flottante en même temps qu'une multiplication flottante. Les autres circuits de calcul flottant sont répartis sur ces deux ports d'émission flottants. L'avantage est que cela se marie bien avec l'usage d'une fenêtre d'instruction séparée pour les opérations flottantes. La fenêtre d'instruction a alors deux ports séparés, au lieu d'un seul. Rajouter un second port d'émission flottant n'est pas trop un problème, car le cout lié à l'ajout d'un port n'est pas linéaire. Passer de un port à deux a un cout tolérable, bien plus que de passer de 3 ports à 4 ou de 4 à 5. Il est possible de dupliquer l'additionneur et le multiplieur flottant, ce qui permet d'émettre deux additions et multiplications flottantes en même temps. C'est ce qui est fait sur les processeurs AMD d'architecture Zen 1 et 2. Ils ont deux additionneurs flottants par cœur, deux multiplieurs flottants, chacun avec leur propre port d'émission. Les performances en calcul flottant sont assez impressionnantes pour un processeur de l'époque. [[File:ZEN - émission multiple flottante.png|centre|vignette|upright=2|Microarchitecture Zen 1 d'AMD.]] ==Les ports d'émission et de décodage== Les ports d'émission et de décodage sont des ressources matérielles comme les autres. Dans cette section, nous allons parler de concepts liés aux ports d'émission et de décodage. Nous allons notamment parler du partage des ports d'émission, ainsi que des relations numériques entre ces deux types de ports. ===Le partage des ports d'émission et la ''port contention''=== Il est possible de partager un port d'émission, à savoir de connecter un port d'émission à plusieurs unités de calcul. Nous en avons vu un exemple plus haut, avec l'émission multiple des opérations entières. On peut par exemple connecter un multiplieur et une ALU entière sur le même port d'émission. Pour le moment, nous n'avons vu que des cas où un port d'émission est relié soit à des ALU entières, soit à des FPU, pas aux deux. Mais il existe des processeurs où un même port d'émission alimente à la fois une ALU entière et une FPU. Par exemple, on peut relier un additionneur flottant sur le même port qu'une ALU entière. Il faut noter que cela implique une fenêtre d'instruction centralisée, capable de mettre en attente micro-opérations entières et flottantes. Un exemple est celui des processeurs Core 2 Duo. Ils disposent de 6 ports d'émission, dont 3 ports dédiés à l'unité mémoire. Les 3 ports restants alimentent chacun : une ALU entière, un circuit de calcul flottant et une unité de calcul SSE (une unité de calcul SIMD, qu'on abordera dans quelques chapitres). * Le premier port alimente une ALU entière simple et un multiplieur/diviseur flottant. * Le second alimente une ALU entière, un multiplieur entier et un additionneur flottant. * Le troisième alimente une ALU entière, sans circuit flottant dédié. Dans le même genre, il est possible de partager un port d'émission entre l'unité mémoire et une ALU entière. Cela permet d'utiliser l'ALU entière pour les calculs d'adresse, ce qui évite d'avoir à utiliser une unité de calcul d'adresse distincte. Un exemple est celui du processeur superscalaire double émission Power PC 440. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. L'organisation en question permet soit d'émettre un accès mémoire en même temps qu'une opération entière, soit d'émettre deux opérations entières simples, soit d’émettre une multiplication et une addition/soustraction/comparaison. Une organisation simple, mais très efficace ! [[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]] Regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles, qui ne se présentent que pour certaines combinaisons de micro-opérations bien précises. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port. Le nombre de ports peut être un facteur limitant pour la performance dans certaines situations de '''''port contention''''' où on a assez d'ALU pour exécuter N micro-opérations, mais où la répartition des ALUs sur les ports l’empêche. Mais on peut réduire la ''port contention'' en répartissant correctement les ALU sur les ports d'émission. Le choix en question dépendent fortement du fait qu'en moyenne, que certaines paires d'instructions sont plus fréquentes que d'autres. Tout cela guide ce choix de répartition des ALUs sur les ports. Nous venons de voir que la répartition des ALU sur les ports d'émission est très variable d'un processeur à l'autre. Les divers choix possibles sont tous des compromis entre deux forces : réduire le nombre de ports d'émission d'un côté, garder de bonnes performances en réduisant la ''port contention'' de l'autre. Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction relativement simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ? Par contre, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal réparti les ALU. ===La séparation entre largeur de décodage et d'émission=== Sur les processeurs superscalaires larges, le processeur peut émettre plus d'instructions qu'il peut en décoder. Plus haut, j'ai dit que l'unité d'émission avait parfois plus de ports d'émission que de ports de décodage. La raison est assez simple, et elle s'explique assez bien avec un exemple. Prenons un processeur avec trois ALU entières, une FPU, une unité de branchement et une unité mémoire. Chacune a son propre port d'émission, ce qui fait que le CPU est capable d'émettre 6 µops. Du moins, c'est la théorie, car les conditions pour sont assez rares. Il faut qu'en chargeant un paquet de 6 instructions, il y ait exactement trois instructions entières, une flottante, un branchement et un accès mémoire. Si le paquet contient 5 instructions entières et un branchement, seules quatre instructions sont émises. Dans cet exemple, dans la grande majorité des cas, seuls 4 ports seront utilisés en moyenne et deux seront sous-utilisés. Il s'agit là d'une moyenne, qui dépend du code, et des instructions exactes à exécuter. Cependant, cette moyenne nous donne un moyen d'économiser beaucoup de circuits pour un cout en performance mineur. L'idée est de ne charger que des blocs de 4 instructions, mais d'avoir 6 ports d'émission. Le processeur charge et décode 4 instructions, les quatre instructions sont envoyées sur les ports d'émission adéquat. Le cout en transistors est alors situé entre celui d'un CPU quadruple émission et sextuple émission. Et les performances sont aussi entre les deux. Pour les performances, c'est parce que cela permet d'avoir un CPU quadruple émission avec bien moins de contraintes d’appariement. Imaginez qu'on se soit limité à quatre ports d'émission : il aurait fallu mettre plusieurs unités sur le même port d'émission. En conséquence, il y aurait eu des combinaisons interdites, comme émettre deux instructions entières + un branchement + un accès mémoire ; ou deux instructions entières + une opération flottante + un accès mémoire. De tells appariement interdits sont possibles en utilisant les six ports d'émission. ==L'émission multiple des accès mémoire== Les premiers processeurs superscalaires ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles. Par contre, c'est autre chose sur les processeurs superscalaires larges. Vu qu'ils travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares. L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. La ''load/store queue'' reste une structure unique, mais on peut lui rajouter des ports. Et à ce peit jeu, il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire. La première duplique les unités de calcul, mais pas les ports d'accès au cache. Les accès mémoire sont alors accumulés dans la ''load/store queue'', qui sont ensuite traités un par un par le cache. Un processeur utilisant cette technique est, par exemple, capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. Un exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. Ces processeurs pouvaient calculer trois adresses simultanément, mais ne pouvaient faire qu'un seul accès au cache par cycle. Le cout en circuit est modéré : les unités de calcul coutent très peu, les ports de la ''load/store queue'' coutent déjà un peu plus. La seconde méthode ajoute des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultanées. Il faut alors dupliquer des unités de calcul, rajouter les ports sur le cache, mais aussi rajouter des ports sur la ''load/store queue'', pour qu'elle échange des données/commandes mémoire avec le cache. Le cout en circuits est alors conséquent Quelques processeurs utilisaient des solutions intermédiaires, un exemple classique étant les Pentium 1 et 2 d'Intel. Mais nous en parlerons dans le chapitre suivant. [[File:Processeur non-superscalaire basique 02.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]] Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre. Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Le Pentium 4 était dans ce cas, mais il était un peu seul dans sa catégorie. Un cas similaire est celui où l'unité mémoire est alimentée par une fenêtre de micro-opération mélangeant instructions mémoire et entières. Les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. Dans ce cas, l'émission multiple est rarement implémentée. Le cas suivant a une file pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique. Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui pouvaient seulement émettre une lecture en même temps qu'une écriture. Ils avaient deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse. [[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]] Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture demande d'utiliser une file d'écriture multiport, ce qui est assez compliqué. Implémenter des lectures en plus est plus simple : il suffit de rajouter une seconde file de µops LOAD et une unité de calcul, alimentées via un second port d'émission. Il est aussi possible de rajouter un second port de lecture au cache, mais ce n'est pas obligatoire. Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse. De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports de lecture et d'écriture car il y a plus de lecture que d'écritures. Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. : Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse. [[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]] Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettent plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail. Pour résumer, l'évolution des CPU superscalaires a été la suivante : * pipeline mémorie fusionné avec le pipeline entier ; * séparation du pipeline mémoire du reste, avec émission parallèle des accès mémoire ; * émission multiple, mais sans dupliquer les ports du cache ; * émission multiple, avec émission simultanée d'une lecture et d’une écriture dans le cache ; * émission multiple avec plusieurs accès en lecture au cache. ==Les optimisations des processeurs superscalaires larges== Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul. ===L'étape de chargement d'un CPU superscalaire large=== Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 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 autoaligné.]] Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements ! [[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]] Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez 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 microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total). Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois 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 microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement. Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations. ===La micro-fusion des processeurs Intel et AMD=== La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''. Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''. Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout. La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Le parallélisme mémoire | prevText=Le parallélisme mémoire | next=Exemples de microarchitectures CPU : le cas du x86 | nextText=Exemples de microarchitectures CPU : le cas du x86 }} </noinclude> 8qokyqycibysrrkyxbji6ecn38n6oyk 772695 772694 2026-09-21T16:42:34Z Mewtow 31375 /* Les unités de calcul d'un processeur superscalaire */ 772695 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 tout premiers processeurs superscalaires pouvaient émettre deux µops en même temps, ce qui portait le nom de '''double émission'''. Mais le nombre a rapidement augmenté et les processeurs modernes 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 des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. Il y a donc des paires d'instructions autorisées et des paires interdites. En pratique, ces contraintes d’appariement ont peu d’impact. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière, le ratio est le même quand on remplace la multiplication par un branchement ou un accès mémoire. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Deux branchements consécutifs sont quant à eux plus fréquents, idem pour deux accès mémoire. Dupliquer des ALU entières est donc la priorité, suivi par la duplication des unités de branchement, puis celle des accès mémoire, pour terminer par le multiplieur et la FPU. ==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. Par exemple, l'unité de chargement charge deux fois plus d'instructions, mais n'est pas modifiée en profondeur. L'unité d'émission et les circuits d'exécution dans le désordre sont eux aussi altérés. Les unités de calcul sont souvent dupliquées, sauf sur les tout premiers CPU superscalaires. {|class="wikitable" |- ! rowspan="2" | Processeur sans émission multiple | rowspan="2" | Chargement | rowspan="2" | Décodage | rowspan="2" | Renommage | rowspan="2" | Émission | Exécution / ALU | rowspan="2" | ''Commit''/ROB |- | Exécution / ALU |- ! rowspan="4" | Processeur superscalaire | rowspan="4" | Chargement | rowspan="2" | Décodage | rowspan="4" | Renommage | rowspan="4" | Émission | Exécution / ALU | rowspan="4" | ''Commit''/ROB |- | Exécution / ALU |- | rowspan="2" | Décodage | Exécution / ALU |- | Exécution / ALU |} Voyons ce qu'il en est dans le détail. ===Les unités de calcul d'un processeur superscalaire=== Émettre N instructions simultanément demande implicitement d'exécuter N instructions par cycle, voire plus si des instructions multicycles viennent mettre leur grain de sel. 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 basique, avec seulement des ALU entières, peut se le permettre. Du moins, à condition de ne pas dupliquer l'unité mémoire, ce qui fait qu'il a pas mal de contraintes d'appariement. Par exemple, un processeur RISC basique avec seulement une ALU entière et un ''barrel shifter'' peut être rendu superscalaire en dupliquant les deux circuits. Le problème, est que cette solution marche assez mal dès que le processeur contient des circuits multiplieurs et/ou une FPU. Par exemple, un processeur à double émission devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, etc. 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 ! Par contre, l'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission. Un autre cas extrême est celui où aucune unité de calcul n'est dupliquée. Pour comprendre comment c'est possible rappelez-vous qu'un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et éventuellement une unité de branchement. Cela suffit pour créer un processeur quadruple émission, très limité. Un tel processeur peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire. Le problème est qu'il est rare que l'on ait à exécuter en même temps un calcul entier, un calcul flottant, un accès mémoire et un branchement. La quadruple émission n'est donc que rarement utilisée. En pratique, les seules combinaisons fréquentes sont les combinaisons calcul + accès mémoire et calcul + branchement. Les combinaisons calcul entier + calcul flottant sont aussi possibles, mais plus rares. Mais on dépasse rarement la double émission. En clair, à partir de la quadruple émission, on ne peut plus se contenter d'une ALU, d'une FPU, d'une unité de branchement et d'une unité mémoire. Les processeurs superscalaires larges n'ont pas le choix que de rajouter des unités de calcul. Reste à voir ce qui est dupliqué en pratique. 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. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant. ===Le chargement de plusieurs instructions consécutives=== Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large. Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence. Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas. Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement non-pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc. [[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]] ===Le décodage parallèle des instructions=== Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits. Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur. Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! ===L'unité d'émission d'un processeur superscalaire=== La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. Nous verrons pourquoi dans la suite, mais on peut dire que c'est juste plus simple et plus efficace ainsi. Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop candidate, sous entendu "candidate à l'émission". L'unité d'émission détecte les dépendances entre la µop candidate et les µops en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en cours d'exécution dans le pipeline. mais en plus, elle doit tester les dépendances entre les µops candidates. Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément, il doit détecter les paires d'instructions dépendantes et les sérialiser. Avec une fenêtre d'instruction, il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil gérent les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires modernes utilisent tous une fenêtre d'instruction centralisée ou décentralisée, et non des ''scoreboard''. Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps, là encore en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire peut émettre 8 µops à la fois, on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle. ===L'unité de renommage superscalaire=== Avec la superscalarité, l'unité de renommage de registre doit gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance de registre, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées. Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat. [[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]] Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante. [[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]] Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande. [[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]] ===Le banc de registre d'un processeur superscalaire=== Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement. 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. ==L'évolution historique des processeurs superscalaires== Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs triple émission permettent eux d’émettre trois instructions à la fois. Les tout premiers processeurs superscalaires avaient un budget en transistors très limité, ce qui fait qu'ils ne pouvaient pas être "totalement superscalaires". Par exemple, un processeur double émission ne pouvait pas émettre deux opérations entières en même temps, ni deux opérations flottantes, ni une opération entière avec un accès mémoire. Il y avait des paires d'instructions autorisées et des paires interdites. Les '''contraintes d'appariement''' sur les instructions à émettre en même temps étaient très strictes. L'intérêt de ces contraintes d’appariement est que le processeur était très simple, il n'y avait pas besoin de rajouter des circuits complexes pour rendre le processeur superscalaire. Les CPU superscalaires historiques ne faisaient que mieux utiliser des unités de calcul déjà présentes. Prenons un processeur non-superscalaire simple, illustré ci-dessous. Il contient : une unité de chargement, un décodeur, une unité d'émission, une ALU, une FPU, les bancs de registre entier et flottants, et une unité mémoire. Nous avons placé l'unité mémoire à la suite de l'ALU entière, car ce sera important pour les explications qui vont suivre. [[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]] Les processeurs superscalaires historiques modifiaient assez peu le schéma précédent. L'unité de chargement était adaptée pour lire plusieurs instructions depuis la mémoire, il y avait un seul décodeur, l'unité d'émission était assez peu modifiée. Mais une chose était certaine : aucune unité de calcul n'était dupliquée, le banc de registre n'était presque pas modifié. A la rigueur, gérer la triple 'émission avec les branchements demandait parfois d'ajouter des ports de lecture sur le banc de registre généraux, pas plus. ===La double émission entière-flottante=== Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est d'émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Le processeur a donc deux pipelines séparés : un pour les micro-opérations entières, un autre pour les micro-opérations flottantes. On parle alors de '''double émission entière-flottante'''. {|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, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité. La plupart des premiers processeurs superscalaires étaient de ce type. Les exemples les plus notables sont les processeurs POWER 1 et ses dérivés comme le ''RISC Single Chip''. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle. [[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]] La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisations visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate]. ===L'émission parallèle des branchements=== Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur. 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 modifier le banc de registres. Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. [[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]] Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications et les divisions ? Pour étudier ces questions, nous allons prendre le cas d'un processeur capable d’émettre trois opérations en même temps : deux opérations entières, et une flottante, potentiellement d'autres. {|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 |} 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|Emission multiple des opérations entières, implémentation courante]] Pour les branchements, deux solutions sont possibles. Avec la première, il y a émission parallèle des branchements, à savoir qu'il est possible d'émettre en branchement en parallèle de deux-trois instructions entières. Et cela impose la présence d'une unité de branchement séparée, avec son port d'émission. Avec la seconde, les branchements sont traités comme des instructions entières. Soit les branchements sont exécutés dans une ALU entière, soit il y a une unité de branchement reliée à un port d'émission pour les opérations entières. [[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Emission série et parallèle des branchements]] Avec la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales, sur les processeurs superscalaires historiques. En clair, le processeur peut émettre 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. De plus, 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. Ce compromis change cependant quand on passe à la quadruple émission. {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 |- | * ALU entière * Multiplieur | * ALU entière * Barrel shifter * Unité de branchement * Unité mémoire | FPU |- | * Additions, soustractions, opérations bit à bit, etc. * Multiplication | * Additions, soustractions, opérations bit à bit, etc. * Décalages et rotations * Branchements * Accès mémoire |Opération flottante |} ===Les CPU superscalaires à plusieurs unités spécialisées=== Pour la triple émission, deux opérations entières + une flottante est l'idéal. Même les processeurs modernes à triple émission restent sur cette "norme". Mais arrivé à la quadruple émission, les CPU superscalaires ont évolués dans des directions différentes, chacun faisant à sa sauce. La raison est que la quadruple émission permet de nombreuses possibilités, mais la plupart ne sont pas intéressantes. Prenons un CPU superscalaire avec une ALU entière, une FPU, une unité de branchement, et une unité mémoire. Il est possible de créer un CPU quadruple émission avec ces unités, mais le gain en performance n'en vaudrait pas la peine. Il est rare qu'un bloc de 4 instructions contienne exactement une opération entière, une flottante, un branchement et un accès mémoire. En pratique, les seules combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. En conséquence, une fois arrivé à la quadruple émission, les processeurs superscalaires n'ont pas le choix que de rajouter des unités de calcul, voire des unités mémoire. Et ils ont privilégié l'ajout d'ALU entières. La raison est que les programmes informatiques utilisent beaucoup plus d'additions/soustractions que le reste. Il y a environ 5 opérations entières de base pour une multiplication entière, le ratio est le même quand on remplace la multiplication par un branchement ou un accès mémoire. En conséquence, les ALU entières ont été dupliquées en premier, et dans de nombreux exemplaires. Reste à voir comment a été dupliqué le reste. Les multiplieurs n'ont pas été dupliqués, sauf sur les processeurs superscalaires très larges. La raison est qu'il est rare de devoir exécuter deux multiplications à la fois. Et c'est la même chose pour le ''barrel shifter''. Deux branchements consécutifs sont quant à eux plus fréquents, c'est même assez courant. Aussi, les unités de branchement ont été dupliquées en second lieu, après les ALU entières. Avoir plusieurs accès mémoire proches est assez courant, surtout sur les CPU de type CISC. Aussi, les unités mémoire ont été dupliquées en troisième lieu. Pour résumer, dupliquer des ALU entières est donc la priorité, suivi par la duplication des unités de branchement, puis celle des accès mémoire, pour terminer par le multiplieur et le ''barrel shifter''. La FPU est un peu à part. Une autre tendance a été de séparer l'unité mémoire des ALU entières, ce qui a permis l''''émission parallèle des µops mémoire'''. Comme son nom l'indique, l'idée est d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière. La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple. La '''double émission flottante''' a été utilisée sur les processeurs Alpha. L'idée est d'émettre deux opérations flottantes à la fois. Sauf qu'une implémentation naïve demanderait de dupliquer la FPU, ce qui aurait un cout en circuit rédhibitoire. Pour rappel, les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant. L'idée est d'avoir des ports d'émission séparés pour l'additionneur flottant et le multiplieur flottant. Le processeur peut alors émettre une addition flottante en même temps qu'une multiplication flottante. Les autres circuits de calcul flottant sont répartis sur ces deux ports d'émission flottants. L'avantage est que cela se marie bien avec l'usage d'une fenêtre d'instruction séparée pour les opérations flottantes. La fenêtre d'instruction a alors deux ports séparés, au lieu d'un seul. Rajouter un second port d'émission flottant n'est pas trop un problème, car le cout lié à l'ajout d'un port n'est pas linéaire. Passer de un port à deux a un cout tolérable, bien plus que de passer de 3 ports à 4 ou de 4 à 5. Il est possible de dupliquer l'additionneur et le multiplieur flottant, ce qui permet d'émettre deux additions et multiplications flottantes en même temps. C'est ce qui est fait sur les processeurs AMD d'architecture Zen 1 et 2. Ils ont deux additionneurs flottants par cœur, deux multiplieurs flottants, chacun avec leur propre port d'émission. Les performances en calcul flottant sont assez impressionnantes pour un processeur de l'époque. [[File:ZEN - émission multiple flottante.png|centre|vignette|upright=2|Microarchitecture Zen 1 d'AMD.]] ==Les ports d'émission et de décodage== Les ports d'émission et de décodage sont des ressources matérielles comme les autres. Dans cette section, nous allons parler de concepts liés aux ports d'émission et de décodage. Nous allons notamment parler du partage des ports d'émission, ainsi que des relations numériques entre ces deux types de ports. ===Le partage des ports d'émission et la ''port contention''=== Il est possible de partager un port d'émission, à savoir de connecter un port d'émission à plusieurs unités de calcul. Nous en avons vu un exemple plus haut, avec l'émission multiple des opérations entières. On peut par exemple connecter un multiplieur et une ALU entière sur le même port d'émission. Pour le moment, nous n'avons vu que des cas où un port d'émission est relié soit à des ALU entières, soit à des FPU, pas aux deux. Mais il existe des processeurs où un même port d'émission alimente à la fois une ALU entière et une FPU. Par exemple, on peut relier un additionneur flottant sur le même port qu'une ALU entière. Il faut noter que cela implique une fenêtre d'instruction centralisée, capable de mettre en attente micro-opérations entières et flottantes. Un exemple est celui des processeurs Core 2 Duo. Ils disposent de 6 ports d'émission, dont 3 ports dédiés à l'unité mémoire. Les 3 ports restants alimentent chacun : une ALU entière, un circuit de calcul flottant et une unité de calcul SSE (une unité de calcul SIMD, qu'on abordera dans quelques chapitres). * Le premier port alimente une ALU entière simple et un multiplieur/diviseur flottant. * Le second alimente une ALU entière, un multiplieur entier et un additionneur flottant. * Le troisième alimente une ALU entière, sans circuit flottant dédié. Dans le même genre, il est possible de partager un port d'émission entre l'unité mémoire et une ALU entière. Cela permet d'utiliser l'ALU entière pour les calculs d'adresse, ce qui évite d'avoir à utiliser une unité de calcul d'adresse distincte. Un exemple est celui du processeur superscalaire double émission Power PC 440. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. L'organisation en question permet soit d'émettre un accès mémoire en même temps qu'une opération entière, soit d'émettre deux opérations entières simples, soit d’émettre une multiplication et une addition/soustraction/comparaison. Une organisation simple, mais très efficace ! [[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]] Regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles, qui ne se présentent que pour certaines combinaisons de micro-opérations bien précises. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port. Le nombre de ports peut être un facteur limitant pour la performance dans certaines situations de '''''port contention''''' où on a assez d'ALU pour exécuter N micro-opérations, mais où la répartition des ALUs sur les ports l’empêche. Mais on peut réduire la ''port contention'' en répartissant correctement les ALU sur les ports d'émission. Le choix en question dépendent fortement du fait qu'en moyenne, que certaines paires d'instructions sont plus fréquentes que d'autres. Tout cela guide ce choix de répartition des ALUs sur les ports. Nous venons de voir que la répartition des ALU sur les ports d'émission est très variable d'un processeur à l'autre. Les divers choix possibles sont tous des compromis entre deux forces : réduire le nombre de ports d'émission d'un côté, garder de bonnes performances en réduisant la ''port contention'' de l'autre. Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction relativement simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ? Par contre, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal réparti les ALU. ===La séparation entre largeur de décodage et d'émission=== Sur les processeurs superscalaires larges, le processeur peut émettre plus d'instructions qu'il peut en décoder. Plus haut, j'ai dit que l'unité d'émission avait parfois plus de ports d'émission que de ports de décodage. La raison est assez simple, et elle s'explique assez bien avec un exemple. Prenons un processeur avec trois ALU entières, une FPU, une unité de branchement et une unité mémoire. Chacune a son propre port d'émission, ce qui fait que le CPU est capable d'émettre 6 µops. Du moins, c'est la théorie, car les conditions pour sont assez rares. Il faut qu'en chargeant un paquet de 6 instructions, il y ait exactement trois instructions entières, une flottante, un branchement et un accès mémoire. Si le paquet contient 5 instructions entières et un branchement, seules quatre instructions sont émises. Dans cet exemple, dans la grande majorité des cas, seuls 4 ports seront utilisés en moyenne et deux seront sous-utilisés. Il s'agit là d'une moyenne, qui dépend du code, et des instructions exactes à exécuter. Cependant, cette moyenne nous donne un moyen d'économiser beaucoup de circuits pour un cout en performance mineur. L'idée est de ne charger que des blocs de 4 instructions, mais d'avoir 6 ports d'émission. Le processeur charge et décode 4 instructions, les quatre instructions sont envoyées sur les ports d'émission adéquat. Le cout en transistors est alors situé entre celui d'un CPU quadruple émission et sextuple émission. Et les performances sont aussi entre les deux. Pour les performances, c'est parce que cela permet d'avoir un CPU quadruple émission avec bien moins de contraintes d’appariement. Imaginez qu'on se soit limité à quatre ports d'émission : il aurait fallu mettre plusieurs unités sur le même port d'émission. En conséquence, il y aurait eu des combinaisons interdites, comme émettre deux instructions entières + un branchement + un accès mémoire ; ou deux instructions entières + une opération flottante + un accès mémoire. De tells appariement interdits sont possibles en utilisant les six ports d'émission. ==L'émission multiple des accès mémoire== Les premiers processeurs superscalaires ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles. Par contre, c'est autre chose sur les processeurs superscalaires larges. Vu qu'ils travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares. L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. La ''load/store queue'' reste une structure unique, mais on peut lui rajouter des ports. Et à ce peit jeu, il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire. La première duplique les unités de calcul, mais pas les ports d'accès au cache. Les accès mémoire sont alors accumulés dans la ''load/store queue'', qui sont ensuite traités un par un par le cache. Un processeur utilisant cette technique est, par exemple, capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. Un exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. Ces processeurs pouvaient calculer trois adresses simultanément, mais ne pouvaient faire qu'un seul accès au cache par cycle. Le cout en circuit est modéré : les unités de calcul coutent très peu, les ports de la ''load/store queue'' coutent déjà un peu plus. La seconde méthode ajoute des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultanées. Il faut alors dupliquer des unités de calcul, rajouter les ports sur le cache, mais aussi rajouter des ports sur la ''load/store queue'', pour qu'elle échange des données/commandes mémoire avec le cache. Le cout en circuits est alors conséquent Quelques processeurs utilisaient des solutions intermédiaires, un exemple classique étant les Pentium 1 et 2 d'Intel. Mais nous en parlerons dans le chapitre suivant. [[File:Processeur non-superscalaire basique 02.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]] Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre. Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Le Pentium 4 était dans ce cas, mais il était un peu seul dans sa catégorie. Un cas similaire est celui où l'unité mémoire est alimentée par une fenêtre de micro-opération mélangeant instructions mémoire et entières. Les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. Dans ce cas, l'émission multiple est rarement implémentée. Le cas suivant a une file pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique. Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui pouvaient seulement émettre une lecture en même temps qu'une écriture. Ils avaient deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse. [[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]] Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture demande d'utiliser une file d'écriture multiport, ce qui est assez compliqué. Implémenter des lectures en plus est plus simple : il suffit de rajouter une seconde file de µops LOAD et une unité de calcul, alimentées via un second port d'émission. Il est aussi possible de rajouter un second port de lecture au cache, mais ce n'est pas obligatoire. Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse. De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports de lecture et d'écriture car il y a plus de lecture que d'écritures. Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. : Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse. [[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]] Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettent plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail. Pour résumer, l'évolution des CPU superscalaires a été la suivante : * pipeline mémorie fusionné avec le pipeline entier ; * séparation du pipeline mémoire du reste, avec émission parallèle des accès mémoire ; * émission multiple, mais sans dupliquer les ports du cache ; * émission multiple, avec émission simultanée d'une lecture et d’une écriture dans le cache ; * émission multiple avec plusieurs accès en lecture au cache. ==Les optimisations des processeurs superscalaires larges== Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul. ===L'étape de chargement d'un CPU superscalaire large=== Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 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 autoaligné.]] Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements ! [[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]] Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez 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 microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total). Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois 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 microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement. Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations. ===La micro-fusion des processeurs Intel et AMD=== La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''. Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''. Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout. La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Le parallélisme mémoire | prevText=Le parallélisme mémoire | next=Exemples de microarchitectures CPU : le cas du x86 | nextText=Exemples de microarchitectures CPU : le cas du x86 }} </noinclude> dz1x2tlozfl6uyaayac8k3gpwz0quvc 772696 772695 2026-09-21T17:10:11Z Mewtow 31375 /* L'unité d'émission d'un processeur superscalaire */ 772696 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 tout premiers processeurs superscalaires pouvaient émettre deux µops en même temps, ce qui portait le nom de '''double émission'''. Mais le nombre a rapidement augmenté et les processeurs modernes 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 des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. Il y a donc des paires d'instructions autorisées et des paires interdites. En pratique, ces contraintes d’appariement ont peu d’impact. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière, le ratio est le même quand on remplace la multiplication par un branchement ou un accès mémoire. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Deux branchements consécutifs sont quant à eux plus fréquents, idem pour deux accès mémoire. Dupliquer des ALU entières est donc la priorité, suivi par la duplication des unités de branchement, puis celle des accès mémoire, pour terminer par le multiplieur et la FPU. ==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. Par exemple, l'unité de chargement charge deux fois plus d'instructions, mais n'est pas modifiée en profondeur. L'unité d'émission et les circuits d'exécution dans le désordre sont eux aussi altérés. Les unités de calcul sont souvent dupliquées, sauf sur les tout premiers CPU superscalaires. {|class="wikitable" |- ! rowspan="2" | Processeur sans émission multiple | rowspan="2" | Chargement | rowspan="2" | Décodage | rowspan="2" | Renommage | rowspan="2" | Émission | Exécution / ALU | rowspan="2" | ''Commit''/ROB |- | Exécution / ALU |- ! rowspan="4" | Processeur superscalaire | rowspan="4" | Chargement | rowspan="2" | Décodage | rowspan="4" | Renommage | rowspan="4" | Émission | Exécution / ALU | rowspan="4" | ''Commit''/ROB |- | Exécution / ALU |- | rowspan="2" | Décodage | Exécution / ALU |- | Exécution / ALU |} Voyons ce qu'il en est dans le détail. ===Les unités de calcul d'un processeur superscalaire=== Émettre N instructions simultanément demande implicitement d'exécuter N instructions par cycle, voire plus si des instructions multicycles viennent mettre leur grain de sel. 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 basique, avec seulement des ALU entières, peut se le permettre. Du moins, à condition de ne pas dupliquer l'unité mémoire, ce qui fait qu'il a pas mal de contraintes d'appariement. Par exemple, un processeur RISC basique avec seulement une ALU entière et un ''barrel shifter'' peut être rendu superscalaire en dupliquant les deux circuits. Le problème, est que cette solution marche assez mal dès que le processeur contient des circuits multiplieurs et/ou une FPU. Par exemple, un processeur à double émission devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, etc. 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 ! Par contre, l'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission. Un autre cas extrême est celui où aucune unité de calcul n'est dupliquée. Pour comprendre comment c'est possible rappelez-vous qu'un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et éventuellement une unité de branchement. Cela suffit pour créer un processeur quadruple émission, très limité. Un tel processeur peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire. Le problème est qu'il est rare que l'on ait à exécuter en même temps un calcul entier, un calcul flottant, un accès mémoire et un branchement. La quadruple émission n'est donc que rarement utilisée. En pratique, les seules combinaisons fréquentes sont les combinaisons calcul + accès mémoire et calcul + branchement. Les combinaisons calcul entier + calcul flottant sont aussi possibles, mais plus rares. Mais on dépasse rarement la double émission. En clair, à partir de la quadruple émission, on ne peut plus se contenter d'une ALU, d'une FPU, d'une unité de branchement et d'une unité mémoire. Les processeurs superscalaires larges n'ont pas le choix que de rajouter des unités de calcul. Reste à voir ce qui est dupliqué en pratique. 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. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant. ===Le chargement de plusieurs instructions consécutives=== Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large. Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence. Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas. Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement non-pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc. [[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]] ===Le décodage parallèle des instructions=== Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits. Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur. Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! ===L'unité d'émission d'un processeur superscalaire=== La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. 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 4 instructions, compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément deux addition, un accès mémoire et une opération flottante. Des implémentations avec des contraintes d'appariemment plus strictes sont possibles, mais passons. 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. Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop candidate, sous entendu "candidate à l'émission". L'unité d'émission détecte les dépendances entre la µop candidate et les µops en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en cours d'exécution dans le pipeline. mais en plus, elle doit tester les dépendances entre les µops candidates. Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément, il doit détecter les paires d'instructions dépendantes et les sérialiser. Avec une fenêtre d'instruction, il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil gérent les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires modernes utilisent tous une fenêtre d'instruction centralisée ou décentralisée, et non des ''scoreboard''. Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps, là encore en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire peut émettre 8 µops à la fois, on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle. 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=== Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement. 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. ==L'évolution historique des processeurs superscalaires== Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs triple émission permettent eux d’émettre trois instructions à la fois. Les tout premiers processeurs superscalaires avaient un budget en transistors très limité, ce qui fait qu'ils ne pouvaient pas être "totalement superscalaires". Par exemple, un processeur double émission ne pouvait pas émettre deux opérations entières en même temps, ni deux opérations flottantes, ni une opération entière avec un accès mémoire. Il y avait des paires d'instructions autorisées et des paires interdites. Les '''contraintes d'appariement''' sur les instructions à émettre en même temps étaient très strictes. L'intérêt de ces contraintes d’appariement est que le processeur était très simple, il n'y avait pas besoin de rajouter des circuits complexes pour rendre le processeur superscalaire. Les CPU superscalaires historiques ne faisaient que mieux utiliser des unités de calcul déjà présentes. Prenons un processeur non-superscalaire simple, illustré ci-dessous. Il contient : une unité de chargement, un décodeur, une unité d'émission, une ALU, une FPU, les bancs de registre entier et flottants, et une unité mémoire. Nous avons placé l'unité mémoire à la suite de l'ALU entière, car ce sera important pour les explications qui vont suivre. [[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]] Les processeurs superscalaires historiques modifiaient assez peu le schéma précédent. L'unité de chargement était adaptée pour lire plusieurs instructions depuis la mémoire, il y avait un seul décodeur, l'unité d'émission était assez peu modifiée. Mais une chose était certaine : aucune unité de calcul n'était dupliquée, le banc de registre n'était presque pas modifié. A la rigueur, gérer la triple 'émission avec les branchements demandait parfois d'ajouter des ports de lecture sur le banc de registre généraux, pas plus. ===La double émission entière-flottante=== Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est d'émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Le processeur a donc deux pipelines séparés : un pour les micro-opérations entières, un autre pour les micro-opérations flottantes. On parle alors de '''double émission entière-flottante'''. {|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, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité. La plupart des premiers processeurs superscalaires étaient de ce type. Les exemples les plus notables sont les processeurs POWER 1 et ses dérivés comme le ''RISC Single Chip''. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle. [[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]] La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisations visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate]. ===L'émission parallèle des branchements=== Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur. 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 modifier le banc de registres. Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. [[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]] Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications et les divisions ? Pour étudier ces questions, nous allons prendre le cas d'un processeur capable d’émettre trois opérations en même temps : deux opérations entières, et une flottante, potentiellement d'autres. {|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 |} 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|Emission multiple des opérations entières, implémentation courante]] Pour les branchements, deux solutions sont possibles. Avec la première, il y a émission parallèle des branchements, à savoir qu'il est possible d'émettre en branchement en parallèle de deux-trois instructions entières. Et cela impose la présence d'une unité de branchement séparée, avec son port d'émission. Avec la seconde, les branchements sont traités comme des instructions entières. Soit les branchements sont exécutés dans une ALU entière, soit il y a une unité de branchement reliée à un port d'émission pour les opérations entières. [[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Emission série et parallèle des branchements]] Avec la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales, sur les processeurs superscalaires historiques. En clair, le processeur peut émettre 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. De plus, 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. Ce compromis change cependant quand on passe à la quadruple émission. {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 |- | * ALU entière * Multiplieur | * ALU entière * Barrel shifter * Unité de branchement * Unité mémoire | FPU |- | * Additions, soustractions, opérations bit à bit, etc. * Multiplication | * Additions, soustractions, opérations bit à bit, etc. * Décalages et rotations * Branchements * Accès mémoire |Opération flottante |} ===Les CPU superscalaires à plusieurs unités spécialisées=== Pour la triple émission, deux opérations entières + une flottante est l'idéal. Même les processeurs modernes à triple émission restent sur cette "norme". Mais arrivé à la quadruple émission, les CPU superscalaires ont évolués dans des directions différentes, chacun faisant à sa sauce. La raison est que la quadruple émission permet de nombreuses possibilités, mais la plupart ne sont pas intéressantes. Prenons un CPU superscalaire avec une ALU entière, une FPU, une unité de branchement, et une unité mémoire. Il est possible de créer un CPU quadruple émission avec ces unités, mais le gain en performance n'en vaudrait pas la peine. Il est rare qu'un bloc de 4 instructions contienne exactement une opération entière, une flottante, un branchement et un accès mémoire. En pratique, les seules combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. En conséquence, une fois arrivé à la quadruple émission, les processeurs superscalaires n'ont pas le choix que de rajouter des unités de calcul, voire des unités mémoire. Et ils ont privilégié l'ajout d'ALU entières. La raison est que les programmes informatiques utilisent beaucoup plus d'additions/soustractions que le reste. Il y a environ 5 opérations entières de base pour une multiplication entière, le ratio est le même quand on remplace la multiplication par un branchement ou un accès mémoire. En conséquence, les ALU entières ont été dupliquées en premier, et dans de nombreux exemplaires. Reste à voir comment a été dupliqué le reste. Les multiplieurs n'ont pas été dupliqués, sauf sur les processeurs superscalaires très larges. La raison est qu'il est rare de devoir exécuter deux multiplications à la fois. Et c'est la même chose pour le ''barrel shifter''. Deux branchements consécutifs sont quant à eux plus fréquents, c'est même assez courant. Aussi, les unités de branchement ont été dupliquées en second lieu, après les ALU entières. Avoir plusieurs accès mémoire proches est assez courant, surtout sur les CPU de type CISC. Aussi, les unités mémoire ont été dupliquées en troisième lieu. Pour résumer, dupliquer des ALU entières est donc la priorité, suivi par la duplication des unités de branchement, puis celle des accès mémoire, pour terminer par le multiplieur et le ''barrel shifter''. La FPU est un peu à part. Une autre tendance a été de séparer l'unité mémoire des ALU entières, ce qui a permis l''''émission parallèle des µops mémoire'''. Comme son nom l'indique, l'idée est d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière. La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple. La '''double émission flottante''' a été utilisée sur les processeurs Alpha. L'idée est d'émettre deux opérations flottantes à la fois. Sauf qu'une implémentation naïve demanderait de dupliquer la FPU, ce qui aurait un cout en circuit rédhibitoire. Pour rappel, les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant. L'idée est d'avoir des ports d'émission séparés pour l'additionneur flottant et le multiplieur flottant. Le processeur peut alors émettre une addition flottante en même temps qu'une multiplication flottante. Les autres circuits de calcul flottant sont répartis sur ces deux ports d'émission flottants. L'avantage est que cela se marie bien avec l'usage d'une fenêtre d'instruction séparée pour les opérations flottantes. La fenêtre d'instruction a alors deux ports séparés, au lieu d'un seul. Rajouter un second port d'émission flottant n'est pas trop un problème, car le cout lié à l'ajout d'un port n'est pas linéaire. Passer de un port à deux a un cout tolérable, bien plus que de passer de 3 ports à 4 ou de 4 à 5. Il est possible de dupliquer l'additionneur et le multiplieur flottant, ce qui permet d'émettre deux additions et multiplications flottantes en même temps. C'est ce qui est fait sur les processeurs AMD d'architecture Zen 1 et 2. Ils ont deux additionneurs flottants par cœur, deux multiplieurs flottants, chacun avec leur propre port d'émission. Les performances en calcul flottant sont assez impressionnantes pour un processeur de l'époque. [[File:ZEN - émission multiple flottante.png|centre|vignette|upright=2|Microarchitecture Zen 1 d'AMD.]] ==Les ports d'émission et de décodage== Les ports d'émission et de décodage sont des ressources matérielles comme les autres. Dans cette section, nous allons parler de concepts liés aux ports d'émission et de décodage. Nous allons notamment parler du partage des ports d'émission, ainsi que des relations numériques entre ces deux types de ports. ===Le partage des ports d'émission et la ''port contention''=== Il est possible de partager un port d'émission, à savoir de connecter un port d'émission à plusieurs unités de calcul. Nous en avons vu un exemple plus haut, avec l'émission multiple des opérations entières. On peut par exemple connecter un multiplieur et une ALU entière sur le même port d'émission. Pour le moment, nous n'avons vu que des cas où un port d'émission est relié soit à des ALU entières, soit à des FPU, pas aux deux. Mais il existe des processeurs où un même port d'émission alimente à la fois une ALU entière et une FPU. Par exemple, on peut relier un additionneur flottant sur le même port qu'une ALU entière. Il faut noter que cela implique une fenêtre d'instruction centralisée, capable de mettre en attente micro-opérations entières et flottantes. Un exemple est celui des processeurs Core 2 Duo. Ils disposent de 6 ports d'émission, dont 3 ports dédiés à l'unité mémoire. Les 3 ports restants alimentent chacun : une ALU entière, un circuit de calcul flottant et une unité de calcul SSE (une unité de calcul SIMD, qu'on abordera dans quelques chapitres). * Le premier port alimente une ALU entière simple et un multiplieur/diviseur flottant. * Le second alimente une ALU entière, un multiplieur entier et un additionneur flottant. * Le troisième alimente une ALU entière, sans circuit flottant dédié. Dans le même genre, il est possible de partager un port d'émission entre l'unité mémoire et une ALU entière. Cela permet d'utiliser l'ALU entière pour les calculs d'adresse, ce qui évite d'avoir à utiliser une unité de calcul d'adresse distincte. Un exemple est celui du processeur superscalaire double émission Power PC 440. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. L'organisation en question permet soit d'émettre un accès mémoire en même temps qu'une opération entière, soit d'émettre deux opérations entières simples, soit d’émettre une multiplication et une addition/soustraction/comparaison. Une organisation simple, mais très efficace ! [[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]] Regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles, qui ne se présentent que pour certaines combinaisons de micro-opérations bien précises. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port. Le nombre de ports peut être un facteur limitant pour la performance dans certaines situations de '''''port contention''''' où on a assez d'ALU pour exécuter N micro-opérations, mais où la répartition des ALUs sur les ports l’empêche. Mais on peut réduire la ''port contention'' en répartissant correctement les ALU sur les ports d'émission. Le choix en question dépendent fortement du fait qu'en moyenne, que certaines paires d'instructions sont plus fréquentes que d'autres. Tout cela guide ce choix de répartition des ALUs sur les ports. Nous venons de voir que la répartition des ALU sur les ports d'émission est très variable d'un processeur à l'autre. Les divers choix possibles sont tous des compromis entre deux forces : réduire le nombre de ports d'émission d'un côté, garder de bonnes performances en réduisant la ''port contention'' de l'autre. Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction relativement simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ? Par contre, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal réparti les ALU. ===La séparation entre largeur de décodage et d'émission=== Sur les processeurs superscalaires larges, le processeur peut émettre plus d'instructions qu'il peut en décoder. Plus haut, j'ai dit que l'unité d'émission avait parfois plus de ports d'émission que de ports de décodage. La raison est assez simple, et elle s'explique assez bien avec un exemple. Prenons un processeur avec trois ALU entières, une FPU, une unité de branchement et une unité mémoire. Chacune a son propre port d'émission, ce qui fait que le CPU est capable d'émettre 6 µops. Du moins, c'est la théorie, car les conditions pour sont assez rares. Il faut qu'en chargeant un paquet de 6 instructions, il y ait exactement trois instructions entières, une flottante, un branchement et un accès mémoire. Si le paquet contient 5 instructions entières et un branchement, seules quatre instructions sont émises. Dans cet exemple, dans la grande majorité des cas, seuls 4 ports seront utilisés en moyenne et deux seront sous-utilisés. Il s'agit là d'une moyenne, qui dépend du code, et des instructions exactes à exécuter. Cependant, cette moyenne nous donne un moyen d'économiser beaucoup de circuits pour un cout en performance mineur. L'idée est de ne charger que des blocs de 4 instructions, mais d'avoir 6 ports d'émission. Le processeur charge et décode 4 instructions, les quatre instructions sont envoyées sur les ports d'émission adéquat. Le cout en transistors est alors situé entre celui d'un CPU quadruple émission et sextuple émission. Et les performances sont aussi entre les deux. Pour les performances, c'est parce que cela permet d'avoir un CPU quadruple émission avec bien moins de contraintes d’appariement. Imaginez qu'on se soit limité à quatre ports d'émission : il aurait fallu mettre plusieurs unités sur le même port d'émission. En conséquence, il y aurait eu des combinaisons interdites, comme émettre deux instructions entières + un branchement + un accès mémoire ; ou deux instructions entières + une opération flottante + un accès mémoire. De tells appariement interdits sont possibles en utilisant les six ports d'émission. ==L'émission multiple des accès mémoire== Les premiers processeurs superscalaires ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles. Par contre, c'est autre chose sur les processeurs superscalaires larges. Vu qu'ils travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares. L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. La ''load/store queue'' reste une structure unique, mais on peut lui rajouter des ports. Et à ce peit jeu, il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire. La première duplique les unités de calcul, mais pas les ports d'accès au cache. Les accès mémoire sont alors accumulés dans la ''load/store queue'', qui sont ensuite traités un par un par le cache. Un processeur utilisant cette technique est, par exemple, capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. Un exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. Ces processeurs pouvaient calculer trois adresses simultanément, mais ne pouvaient faire qu'un seul accès au cache par cycle. Le cout en circuit est modéré : les unités de calcul coutent très peu, les ports de la ''load/store queue'' coutent déjà un peu plus. La seconde méthode ajoute des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultanées. Il faut alors dupliquer des unités de calcul, rajouter les ports sur le cache, mais aussi rajouter des ports sur la ''load/store queue'', pour qu'elle échange des données/commandes mémoire avec le cache. Le cout en circuits est alors conséquent Quelques processeurs utilisaient des solutions intermédiaires, un exemple classique étant les Pentium 1 et 2 d'Intel. Mais nous en parlerons dans le chapitre suivant. [[File:Processeur non-superscalaire basique 02.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]] Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre. Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Le Pentium 4 était dans ce cas, mais il était un peu seul dans sa catégorie. Un cas similaire est celui où l'unité mémoire est alimentée par une fenêtre de micro-opération mélangeant instructions mémoire et entières. Les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. Dans ce cas, l'émission multiple est rarement implémentée. Le cas suivant a une file pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique. Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui pouvaient seulement émettre une lecture en même temps qu'une écriture. Ils avaient deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse. [[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]] Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture demande d'utiliser une file d'écriture multiport, ce qui est assez compliqué. Implémenter des lectures en plus est plus simple : il suffit de rajouter une seconde file de µops LOAD et une unité de calcul, alimentées via un second port d'émission. Il est aussi possible de rajouter un second port de lecture au cache, mais ce n'est pas obligatoire. Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse. De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports de lecture et d'écriture car il y a plus de lecture que d'écritures. Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. : Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse. [[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]] Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettent plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail. Pour résumer, l'évolution des CPU superscalaires a été la suivante : * pipeline mémorie fusionné avec le pipeline entier ; * séparation du pipeline mémoire du reste, avec émission parallèle des accès mémoire ; * émission multiple, mais sans dupliquer les ports du cache ; * émission multiple, avec émission simultanée d'une lecture et d’une écriture dans le cache ; * émission multiple avec plusieurs accès en lecture au cache. ==Les optimisations des processeurs superscalaires larges== Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul. ===L'étape de chargement d'un CPU superscalaire large=== Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 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 autoaligné.]] Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements ! [[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]] Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez 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 microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total). Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois 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 microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement. Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations. ===La micro-fusion des processeurs Intel et AMD=== La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''. Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''. Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout. La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Le parallélisme mémoire | prevText=Le parallélisme mémoire | next=Exemples de microarchitectures CPU : le cas du x86 | nextText=Exemples de microarchitectures CPU : le cas du x86 }} </noinclude> q8ri2qgf4cbakna8nll0lxk16vqyawu 772697 772696 2026-09-21T17:10:14Z Mewtow 31375 /* Le partage des ports d'émission et la port contention */ 772697 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 tout premiers processeurs superscalaires pouvaient émettre deux µops en même temps, ce qui portait le nom de '''double émission'''. Mais le nombre a rapidement augmenté et les processeurs modernes 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 des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. Il y a donc des paires d'instructions autorisées et des paires interdites. En pratique, ces contraintes d’appariement ont peu d’impact. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière, le ratio est le même quand on remplace la multiplication par un branchement ou un accès mémoire. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Deux branchements consécutifs sont quant à eux plus fréquents, idem pour deux accès mémoire. Dupliquer des ALU entières est donc la priorité, suivi par la duplication des unités de branchement, puis celle des accès mémoire, pour terminer par le multiplieur et la FPU. ==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. Par exemple, l'unité de chargement charge deux fois plus d'instructions, mais n'est pas modifiée en profondeur. L'unité d'émission et les circuits d'exécution dans le désordre sont eux aussi altérés. Les unités de calcul sont souvent dupliquées, sauf sur les tout premiers CPU superscalaires. {|class="wikitable" |- ! rowspan="2" | Processeur sans émission multiple | rowspan="2" | Chargement | rowspan="2" | Décodage | rowspan="2" | Renommage | rowspan="2" | Émission | Exécution / ALU | rowspan="2" | ''Commit''/ROB |- | Exécution / ALU |- ! rowspan="4" | Processeur superscalaire | rowspan="4" | Chargement | rowspan="2" | Décodage | rowspan="4" | Renommage | rowspan="4" | Émission | Exécution / ALU | rowspan="4" | ''Commit''/ROB |- | Exécution / ALU |- | rowspan="2" | Décodage | Exécution / ALU |- | Exécution / ALU |} Voyons ce qu'il en est dans le détail. ===Les unités de calcul d'un processeur superscalaire=== Émettre N instructions simultanément demande implicitement d'exécuter N instructions par cycle, voire plus si des instructions multicycles viennent mettre leur grain de sel. 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 basique, avec seulement des ALU entières, peut se le permettre. Du moins, à condition de ne pas dupliquer l'unité mémoire, ce qui fait qu'il a pas mal de contraintes d'appariement. Par exemple, un processeur RISC basique avec seulement une ALU entière et un ''barrel shifter'' peut être rendu superscalaire en dupliquant les deux circuits. Le problème, est que cette solution marche assez mal dès que le processeur contient des circuits multiplieurs et/ou une FPU. Par exemple, un processeur à double émission devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, etc. 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 ! Par contre, l'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission. Un autre cas extrême est celui où aucune unité de calcul n'est dupliquée. Pour comprendre comment c'est possible rappelez-vous qu'un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et éventuellement une unité de branchement. Cela suffit pour créer un processeur quadruple émission, très limité. Un tel processeur peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire. Le problème est qu'il est rare que l'on ait à exécuter en même temps un calcul entier, un calcul flottant, un accès mémoire et un branchement. La quadruple émission n'est donc que rarement utilisée. En pratique, les seules combinaisons fréquentes sont les combinaisons calcul + accès mémoire et calcul + branchement. Les combinaisons calcul entier + calcul flottant sont aussi possibles, mais plus rares. Mais on dépasse rarement la double émission. En clair, à partir de la quadruple émission, on ne peut plus se contenter d'une ALU, d'une FPU, d'une unité de branchement et d'une unité mémoire. Les processeurs superscalaires larges n'ont pas le choix que de rajouter des unités de calcul. Reste à voir ce qui est dupliqué en pratique. 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. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant. ===Le chargement de plusieurs instructions consécutives=== Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large. Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence. Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas. Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement non-pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc. [[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]] ===Le décodage parallèle des instructions=== Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits. Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur. Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! ===L'unité d'émission d'un processeur superscalaire=== La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. 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 4 instructions, compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément deux addition, un accès mémoire et une opération flottante. Des implémentations avec des contraintes d'appariemment plus strictes sont possibles, mais passons. 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. Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop candidate, sous entendu "candidate à l'émission". L'unité d'émission détecte les dépendances entre la µop candidate et les µops en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en cours d'exécution dans le pipeline. mais en plus, elle doit tester les dépendances entre les µops candidates. Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément, il doit détecter les paires d'instructions dépendantes et les sérialiser. Avec une fenêtre d'instruction, il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil gérent les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires modernes utilisent tous une fenêtre d'instruction centralisée ou décentralisée, et non des ''scoreboard''. Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps, là encore en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire peut émettre 8 µops à la fois, on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle. 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=== Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement. 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. ==L'évolution historique des processeurs superscalaires== Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs triple émission permettent eux d’émettre trois instructions à la fois. Les tout premiers processeurs superscalaires avaient un budget en transistors très limité, ce qui fait qu'ils ne pouvaient pas être "totalement superscalaires". Par exemple, un processeur double émission ne pouvait pas émettre deux opérations entières en même temps, ni deux opérations flottantes, ni une opération entière avec un accès mémoire. Il y avait des paires d'instructions autorisées et des paires interdites. Les '''contraintes d'appariement''' sur les instructions à émettre en même temps étaient très strictes. L'intérêt de ces contraintes d’appariement est que le processeur était très simple, il n'y avait pas besoin de rajouter des circuits complexes pour rendre le processeur superscalaire. Les CPU superscalaires historiques ne faisaient que mieux utiliser des unités de calcul déjà présentes. Prenons un processeur non-superscalaire simple, illustré ci-dessous. Il contient : une unité de chargement, un décodeur, une unité d'émission, une ALU, une FPU, les bancs de registre entier et flottants, et une unité mémoire. Nous avons placé l'unité mémoire à la suite de l'ALU entière, car ce sera important pour les explications qui vont suivre. [[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]] Les processeurs superscalaires historiques modifiaient assez peu le schéma précédent. L'unité de chargement était adaptée pour lire plusieurs instructions depuis la mémoire, il y avait un seul décodeur, l'unité d'émission était assez peu modifiée. Mais une chose était certaine : aucune unité de calcul n'était dupliquée, le banc de registre n'était presque pas modifié. A la rigueur, gérer la triple 'émission avec les branchements demandait parfois d'ajouter des ports de lecture sur le banc de registre généraux, pas plus. ===La double émission entière-flottante=== Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est d'émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Le processeur a donc deux pipelines séparés : un pour les micro-opérations entières, un autre pour les micro-opérations flottantes. On parle alors de '''double émission entière-flottante'''. {|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, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité. La plupart des premiers processeurs superscalaires étaient de ce type. Les exemples les plus notables sont les processeurs POWER 1 et ses dérivés comme le ''RISC Single Chip''. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle. [[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]] La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisations visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate]. ===L'émission parallèle des branchements=== Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur. 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 modifier le banc de registres. Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. [[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]] Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications et les divisions ? Pour étudier ces questions, nous allons prendre le cas d'un processeur capable d’émettre trois opérations en même temps : deux opérations entières, et une flottante, potentiellement d'autres. {|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 |} 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|Emission multiple des opérations entières, implémentation courante]] Pour les branchements, deux solutions sont possibles. Avec la première, il y a émission parallèle des branchements, à savoir qu'il est possible d'émettre en branchement en parallèle de deux-trois instructions entières. Et cela impose la présence d'une unité de branchement séparée, avec son port d'émission. Avec la seconde, les branchements sont traités comme des instructions entières. Soit les branchements sont exécutés dans une ALU entière, soit il y a une unité de branchement reliée à un port d'émission pour les opérations entières. [[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Emission série et parallèle des branchements]] Avec la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales, sur les processeurs superscalaires historiques. En clair, le processeur peut émettre 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. De plus, 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. Ce compromis change cependant quand on passe à la quadruple émission. {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 |- | * ALU entière * Multiplieur | * ALU entière * Barrel shifter * Unité de branchement * Unité mémoire | FPU |- | * Additions, soustractions, opérations bit à bit, etc. * Multiplication | * Additions, soustractions, opérations bit à bit, etc. * Décalages et rotations * Branchements * Accès mémoire |Opération flottante |} ===Les CPU superscalaires à plusieurs unités spécialisées=== Pour la triple émission, deux opérations entières + une flottante est l'idéal. Même les processeurs modernes à triple émission restent sur cette "norme". Mais arrivé à la quadruple émission, les CPU superscalaires ont évolués dans des directions différentes, chacun faisant à sa sauce. La raison est que la quadruple émission permet de nombreuses possibilités, mais la plupart ne sont pas intéressantes. Prenons un CPU superscalaire avec une ALU entière, une FPU, une unité de branchement, et une unité mémoire. Il est possible de créer un CPU quadruple émission avec ces unités, mais le gain en performance n'en vaudrait pas la peine. Il est rare qu'un bloc de 4 instructions contienne exactement une opération entière, une flottante, un branchement et un accès mémoire. En pratique, les seules combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. En conséquence, une fois arrivé à la quadruple émission, les processeurs superscalaires n'ont pas le choix que de rajouter des unités de calcul, voire des unités mémoire. Et ils ont privilégié l'ajout d'ALU entières. La raison est que les programmes informatiques utilisent beaucoup plus d'additions/soustractions que le reste. Il y a environ 5 opérations entières de base pour une multiplication entière, le ratio est le même quand on remplace la multiplication par un branchement ou un accès mémoire. En conséquence, les ALU entières ont été dupliquées en premier, et dans de nombreux exemplaires. Reste à voir comment a été dupliqué le reste. Les multiplieurs n'ont pas été dupliqués, sauf sur les processeurs superscalaires très larges. La raison est qu'il est rare de devoir exécuter deux multiplications à la fois. Et c'est la même chose pour le ''barrel shifter''. Deux branchements consécutifs sont quant à eux plus fréquents, c'est même assez courant. Aussi, les unités de branchement ont été dupliquées en second lieu, après les ALU entières. Avoir plusieurs accès mémoire proches est assez courant, surtout sur les CPU de type CISC. Aussi, les unités mémoire ont été dupliquées en troisième lieu. Pour résumer, dupliquer des ALU entières est donc la priorité, suivi par la duplication des unités de branchement, puis celle des accès mémoire, pour terminer par le multiplieur et le ''barrel shifter''. La FPU est un peu à part. Une autre tendance a été de séparer l'unité mémoire des ALU entières, ce qui a permis l''''émission parallèle des µops mémoire'''. Comme son nom l'indique, l'idée est d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière. La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple. La '''double émission flottante''' a été utilisée sur les processeurs Alpha. L'idée est d'émettre deux opérations flottantes à la fois. Sauf qu'une implémentation naïve demanderait de dupliquer la FPU, ce qui aurait un cout en circuit rédhibitoire. Pour rappel, les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant. L'idée est d'avoir des ports d'émission séparés pour l'additionneur flottant et le multiplieur flottant. Le processeur peut alors émettre une addition flottante en même temps qu'une multiplication flottante. Les autres circuits de calcul flottant sont répartis sur ces deux ports d'émission flottants. L'avantage est que cela se marie bien avec l'usage d'une fenêtre d'instruction séparée pour les opérations flottantes. La fenêtre d'instruction a alors deux ports séparés, au lieu d'un seul. Rajouter un second port d'émission flottant n'est pas trop un problème, car le cout lié à l'ajout d'un port n'est pas linéaire. Passer de un port à deux a un cout tolérable, bien plus que de passer de 3 ports à 4 ou de 4 à 5. Il est possible de dupliquer l'additionneur et le multiplieur flottant, ce qui permet d'émettre deux additions et multiplications flottantes en même temps. C'est ce qui est fait sur les processeurs AMD d'architecture Zen 1 et 2. Ils ont deux additionneurs flottants par cœur, deux multiplieurs flottants, chacun avec leur propre port d'émission. Les performances en calcul flottant sont assez impressionnantes pour un processeur de l'époque. [[File:ZEN - émission multiple flottante.png|centre|vignette|upright=2|Microarchitecture Zen 1 d'AMD.]] ==Les ports d'émission et de décodage== Les ports d'émission et de décodage sont des ressources matérielles comme les autres. Dans cette section, nous allons parler de concepts liés aux ports d'émission et de décodage. Nous allons notamment parler du partage des ports d'émission, ainsi que des relations numériques entre ces deux types de ports. ===Le partage des ports d'émission et la ''port contention''=== Pour le moment, nous n'avons vu que des cas où un port d'émission est relié soit à des ALU entières, soit à des FPU, pas aux deux. Mais il existe des processeurs où un même port d'émission alimente à la fois une ALU entière et une FPU. Par exemple, on peut relier un additionneur flottant sur le même port qu'une ALU entière. Il faut noter que cela implique une fenêtre d'instruction centralisée, capable de mettre en attente micro-opérations entières et flottantes. Un exemple est celui des processeurs Core 2 Duo. Ils disposent de 6 ports d'émission, dont 3 ports dédiés à l'unité mémoire. Les 3 ports restants alimentent chacun : une ALU entière, un circuit de calcul flottant et une unité de calcul SSE (une unité de calcul SIMD, qu'on abordera dans quelques chapitres). * Le premier port alimente une ALU entière simple et un multiplieur/diviseur flottant. * Le second alimente une ALU entière, un multiplieur entier et un additionneur flottant. * Le troisième alimente une ALU entière, sans circuit flottant dédié. Dans le même genre, il est possible de partager un port d'émission entre l'unité mémoire et une ALU entière. Cela permet d'utiliser l'ALU entière pour les calculs d'adresse, ce qui évite d'avoir à utiliser une unité de calcul d'adresse distincte. Un exemple est celui du processeur superscalaire double émission Power PC 440. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. L'organisation en question permet soit d'émettre un accès mémoire en même temps qu'une opération entière, soit d'émettre deux opérations entières simples, soit d’émettre une multiplication et une addition/soustraction/comparaison. Une organisation simple, mais très efficace ! [[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]] ===La séparation entre largeur de décodage et d'émission=== Sur les processeurs superscalaires larges, le processeur peut émettre plus d'instructions qu'il peut en décoder. Plus haut, j'ai dit que l'unité d'émission avait parfois plus de ports d'émission que de ports de décodage. La raison est assez simple, et elle s'explique assez bien avec un exemple. Prenons un processeur avec trois ALU entières, une FPU, une unité de branchement et une unité mémoire. Chacune a son propre port d'émission, ce qui fait que le CPU est capable d'émettre 6 µops. Du moins, c'est la théorie, car les conditions pour sont assez rares. Il faut qu'en chargeant un paquet de 6 instructions, il y ait exactement trois instructions entières, une flottante, un branchement et un accès mémoire. Si le paquet contient 5 instructions entières et un branchement, seules quatre instructions sont émises. Dans cet exemple, dans la grande majorité des cas, seuls 4 ports seront utilisés en moyenne et deux seront sous-utilisés. Il s'agit là d'une moyenne, qui dépend du code, et des instructions exactes à exécuter. Cependant, cette moyenne nous donne un moyen d'économiser beaucoup de circuits pour un cout en performance mineur. L'idée est de ne charger que des blocs de 4 instructions, mais d'avoir 6 ports d'émission. Le processeur charge et décode 4 instructions, les quatre instructions sont envoyées sur les ports d'émission adéquat. Le cout en transistors est alors situé entre celui d'un CPU quadruple émission et sextuple émission. Et les performances sont aussi entre les deux. Pour les performances, c'est parce que cela permet d'avoir un CPU quadruple émission avec bien moins de contraintes d’appariement. Imaginez qu'on se soit limité à quatre ports d'émission : il aurait fallu mettre plusieurs unités sur le même port d'émission. En conséquence, il y aurait eu des combinaisons interdites, comme émettre deux instructions entières + un branchement + un accès mémoire ; ou deux instructions entières + une opération flottante + un accès mémoire. De tells appariement interdits sont possibles en utilisant les six ports d'émission. ==L'émission multiple des accès mémoire== Les premiers processeurs superscalaires ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles. Par contre, c'est autre chose sur les processeurs superscalaires larges. Vu qu'ils travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares. L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. La ''load/store queue'' reste une structure unique, mais on peut lui rajouter des ports. Et à ce peit jeu, il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire. La première duplique les unités de calcul, mais pas les ports d'accès au cache. Les accès mémoire sont alors accumulés dans la ''load/store queue'', qui sont ensuite traités un par un par le cache. Un processeur utilisant cette technique est, par exemple, capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. Un exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. Ces processeurs pouvaient calculer trois adresses simultanément, mais ne pouvaient faire qu'un seul accès au cache par cycle. Le cout en circuit est modéré : les unités de calcul coutent très peu, les ports de la ''load/store queue'' coutent déjà un peu plus. La seconde méthode ajoute des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultanées. Il faut alors dupliquer des unités de calcul, rajouter les ports sur le cache, mais aussi rajouter des ports sur la ''load/store queue'', pour qu'elle échange des données/commandes mémoire avec le cache. Le cout en circuits est alors conséquent Quelques processeurs utilisaient des solutions intermédiaires, un exemple classique étant les Pentium 1 et 2 d'Intel. Mais nous en parlerons dans le chapitre suivant. [[File:Processeur non-superscalaire basique 02.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]] Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre. Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Le Pentium 4 était dans ce cas, mais il était un peu seul dans sa catégorie. Un cas similaire est celui où l'unité mémoire est alimentée par une fenêtre de micro-opération mélangeant instructions mémoire et entières. Les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. Dans ce cas, l'émission multiple est rarement implémentée. Le cas suivant a une file pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique. Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui pouvaient seulement émettre une lecture en même temps qu'une écriture. Ils avaient deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse. [[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]] Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture demande d'utiliser une file d'écriture multiport, ce qui est assez compliqué. Implémenter des lectures en plus est plus simple : il suffit de rajouter une seconde file de µops LOAD et une unité de calcul, alimentées via un second port d'émission. Il est aussi possible de rajouter un second port de lecture au cache, mais ce n'est pas obligatoire. Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse. De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports de lecture et d'écriture car il y a plus de lecture que d'écritures. Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. : Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse. [[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]] Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettent plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail. Pour résumer, l'évolution des CPU superscalaires a été la suivante : * pipeline mémorie fusionné avec le pipeline entier ; * séparation du pipeline mémoire du reste, avec émission parallèle des accès mémoire ; * émission multiple, mais sans dupliquer les ports du cache ; * émission multiple, avec émission simultanée d'une lecture et d’une écriture dans le cache ; * émission multiple avec plusieurs accès en lecture au cache. ==Les optimisations des processeurs superscalaires larges== Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul. ===L'étape de chargement d'un CPU superscalaire large=== Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 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 autoaligné.]] Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements ! [[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]] Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez 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 microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total). Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois 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 microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement. Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations. ===La micro-fusion des processeurs Intel et AMD=== La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''. Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''. Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout. La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Le parallélisme mémoire | prevText=Le parallélisme mémoire | next=Exemples de microarchitectures CPU : le cas du x86 | nextText=Exemples de microarchitectures CPU : le cas du x86 }} </noinclude> ovtksyajkhrfjavbhk148ja0csro0xp 772699 772697 2026-09-21T17:21:06Z Mewtow 31375 /* L'unité d'émission d'un processeur superscalaire */ 772699 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 tout premiers processeurs superscalaires pouvaient émettre deux µops en même temps, ce qui portait le nom de '''double émission'''. Mais le nombre a rapidement augmenté et les processeurs modernes 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 des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. Il y a donc des paires d'instructions autorisées et des paires interdites. En pratique, ces contraintes d’appariement ont peu d’impact. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière, le ratio est le même quand on remplace la multiplication par un branchement ou un accès mémoire. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Deux branchements consécutifs sont quant à eux plus fréquents, idem pour deux accès mémoire. Dupliquer des ALU entières est donc la priorité, suivi par la duplication des unités de branchement, puis celle des accès mémoire, pour terminer par le multiplieur et la FPU. ==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. Par exemple, l'unité de chargement charge deux fois plus d'instructions, mais n'est pas modifiée en profondeur. L'unité d'émission et les circuits d'exécution dans le désordre sont eux aussi altérés. Les unités de calcul sont souvent dupliquées, sauf sur les tout premiers CPU superscalaires. {|class="wikitable" |- ! rowspan="2" | Processeur sans émission multiple | rowspan="2" | Chargement | rowspan="2" | Décodage | rowspan="2" | Renommage | rowspan="2" | Émission | Exécution / ALU | rowspan="2" | ''Commit''/ROB |- | Exécution / ALU |- ! rowspan="4" | Processeur superscalaire | rowspan="4" | Chargement | rowspan="2" | Décodage | rowspan="4" | Renommage | rowspan="4" | Émission | Exécution / ALU | rowspan="4" | ''Commit''/ROB |- | Exécution / ALU |- | rowspan="2" | Décodage | Exécution / ALU |- | Exécution / ALU |} Voyons ce qu'il en est dans le détail. ===Les unités de calcul d'un processeur superscalaire=== Émettre N instructions simultanément demande implicitement d'exécuter N instructions par cycle, voire plus si des instructions multicycles viennent mettre leur grain de sel. 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 basique, avec seulement des ALU entières, peut se le permettre. Du moins, à condition de ne pas dupliquer l'unité mémoire, ce qui fait qu'il a pas mal de contraintes d'appariement. Par exemple, un processeur RISC basique avec seulement une ALU entière et un ''barrel shifter'' peut être rendu superscalaire en dupliquant les deux circuits. Le problème, est que cette solution marche assez mal dès que le processeur contient des circuits multiplieurs et/ou une FPU. Par exemple, un processeur à double émission devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, etc. 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 ! Par contre, l'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission. Un autre cas extrême est celui où aucune unité de calcul n'est dupliquée. Pour comprendre comment c'est possible rappelez-vous qu'un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et éventuellement une unité de branchement. Cela suffit pour créer un processeur quadruple émission, très limité. Un tel processeur peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire. Le problème est qu'il est rare que l'on ait à exécuter en même temps un calcul entier, un calcul flottant, un accès mémoire et un branchement. La quadruple émission n'est donc que rarement utilisée. En pratique, les seules combinaisons fréquentes sont les combinaisons calcul + accès mémoire et calcul + branchement. Les combinaisons calcul entier + calcul flottant sont aussi possibles, mais plus rares. Mais on dépasse rarement la double émission. En clair, à partir de la quadruple émission, on ne peut plus se contenter d'une ALU, d'une FPU, d'une unité de branchement et d'une unité mémoire. Les processeurs superscalaires larges n'ont pas le choix que de rajouter des unités de calcul. Reste à voir ce qui est dupliqué en pratique. 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. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant. ===Le chargement de plusieurs instructions consécutives=== Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large. Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence. Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas. Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement non-pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc. [[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]] ===Le décodage parallèle des instructions=== Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits. Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur. Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! ===L'unité d'émission d'un processeur superscalaire=== La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. 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 4 instructions, compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément deux 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. 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. 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 |} 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. Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop candidate, sous entendu "candidate à l'émission". L'unité d'émission détecte les dépendances entre la µop candidate et les µops en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en cours d'exécution dans le pipeline. mais en plus, elle doit tester les dépendances entre les µops candidates. Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément, il doit détecter les paires d'instructions dépendantes et les sérialiser. Avec une fenêtre d'instruction, il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil gérent les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires modernes utilisent tous une fenêtre d'instruction centralisée ou décentralisée, et non des ''scoreboard''. Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps, là encore en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire peut émettre 8 µops à la fois, on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle. 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=== Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement. 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. ==L'évolution historique des processeurs superscalaires== Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs triple émission permettent eux d’émettre trois instructions à la fois. Les tout premiers processeurs superscalaires avaient un budget en transistors très limité, ce qui fait qu'ils ne pouvaient pas être "totalement superscalaires". Par exemple, un processeur double émission ne pouvait pas émettre deux opérations entières en même temps, ni deux opérations flottantes, ni une opération entière avec un accès mémoire. Il y avait des paires d'instructions autorisées et des paires interdites. Les '''contraintes d'appariement''' sur les instructions à émettre en même temps étaient très strictes. L'intérêt de ces contraintes d’appariement est que le processeur était très simple, il n'y avait pas besoin de rajouter des circuits complexes pour rendre le processeur superscalaire. Les CPU superscalaires historiques ne faisaient que mieux utiliser des unités de calcul déjà présentes. Prenons un processeur non-superscalaire simple, illustré ci-dessous. Il contient : une unité de chargement, un décodeur, une unité d'émission, une ALU, une FPU, les bancs de registre entier et flottants, et une unité mémoire. Nous avons placé l'unité mémoire à la suite de l'ALU entière, car ce sera important pour les explications qui vont suivre. [[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]] Les processeurs superscalaires historiques modifiaient assez peu le schéma précédent. L'unité de chargement était adaptée pour lire plusieurs instructions depuis la mémoire, il y avait un seul décodeur, l'unité d'émission était assez peu modifiée. Mais une chose était certaine : aucune unité de calcul n'était dupliquée, le banc de registre n'était presque pas modifié. A la rigueur, gérer la triple 'émission avec les branchements demandait parfois d'ajouter des ports de lecture sur le banc de registre généraux, pas plus. ===La double émission entière-flottante=== Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est d'émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Le processeur a donc deux pipelines séparés : un pour les micro-opérations entières, un autre pour les micro-opérations flottantes. On parle alors de '''double émission entière-flottante'''. {|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, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité. La plupart des premiers processeurs superscalaires étaient de ce type. Les exemples les plus notables sont les processeurs POWER 1 et ses dérivés comme le ''RISC Single Chip''. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle. [[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]] La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisations visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate]. ===L'émission parallèle des branchements=== Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur. 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 modifier le banc de registres. Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. [[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]] Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications et les divisions ? Pour étudier ces questions, nous allons prendre le cas d'un processeur capable d’émettre trois opérations en même temps : deux opérations entières, et une flottante, potentiellement d'autres. {|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 |} 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|Emission multiple des opérations entières, implémentation courante]] Pour les branchements, deux solutions sont possibles. Avec la première, il y a émission parallèle des branchements, à savoir qu'il est possible d'émettre en branchement en parallèle de deux-trois instructions entières. Et cela impose la présence d'une unité de branchement séparée, avec son port d'émission. Avec la seconde, les branchements sont traités comme des instructions entières. Soit les branchements sont exécutés dans une ALU entière, soit il y a une unité de branchement reliée à un port d'émission pour les opérations entières. [[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Emission série et parallèle des branchements]] Avec la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales, sur les processeurs superscalaires historiques. En clair, le processeur peut émettre 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. De plus, 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. Ce compromis change cependant quand on passe à la quadruple émission. {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 |- | * ALU entière * Multiplieur | * ALU entière * Barrel shifter * Unité de branchement * Unité mémoire | FPU |- | * Additions, soustractions, opérations bit à bit, etc. * Multiplication | * Additions, soustractions, opérations bit à bit, etc. * Décalages et rotations * Branchements * Accès mémoire |Opération flottante |} ===Les CPU superscalaires à plusieurs unités spécialisées=== Pour la triple émission, deux opérations entières + une flottante est l'idéal. Même les processeurs modernes à triple émission restent sur cette "norme". Mais arrivé à la quadruple émission, les CPU superscalaires ont évolués dans des directions différentes, chacun faisant à sa sauce. La raison est que la quadruple émission permet de nombreuses possibilités, mais la plupart ne sont pas intéressantes. Prenons un CPU superscalaire avec une ALU entière, une FPU, une unité de branchement, et une unité mémoire. Il est possible de créer un CPU quadruple émission avec ces unités, mais le gain en performance n'en vaudrait pas la peine. Il est rare qu'un bloc de 4 instructions contienne exactement une opération entière, une flottante, un branchement et un accès mémoire. En pratique, les seules combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. En conséquence, une fois arrivé à la quadruple émission, les processeurs superscalaires n'ont pas le choix que de rajouter des unités de calcul, voire des unités mémoire. Et ils ont privilégié l'ajout d'ALU entières. La raison est que les programmes informatiques utilisent beaucoup plus d'additions/soustractions que le reste. Il y a environ 5 opérations entières de base pour une multiplication entière, le ratio est le même quand on remplace la multiplication par un branchement ou un accès mémoire. En conséquence, les ALU entières ont été dupliquées en premier, et dans de nombreux exemplaires. Reste à voir comment a été dupliqué le reste. Les multiplieurs n'ont pas été dupliqués, sauf sur les processeurs superscalaires très larges. La raison est qu'il est rare de devoir exécuter deux multiplications à la fois. Et c'est la même chose pour le ''barrel shifter''. Deux branchements consécutifs sont quant à eux plus fréquents, c'est même assez courant. Aussi, les unités de branchement ont été dupliquées en second lieu, après les ALU entières. Avoir plusieurs accès mémoire proches est assez courant, surtout sur les CPU de type CISC. Aussi, les unités mémoire ont été dupliquées en troisième lieu. Pour résumer, dupliquer des ALU entières est donc la priorité, suivi par la duplication des unités de branchement, puis celle des accès mémoire, pour terminer par le multiplieur et le ''barrel shifter''. La FPU est un peu à part. Une autre tendance a été de séparer l'unité mémoire des ALU entières, ce qui a permis l''''émission parallèle des µops mémoire'''. Comme son nom l'indique, l'idée est d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière. La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple. La '''double émission flottante''' a été utilisée sur les processeurs Alpha. L'idée est d'émettre deux opérations flottantes à la fois. Sauf qu'une implémentation naïve demanderait de dupliquer la FPU, ce qui aurait un cout en circuit rédhibitoire. Pour rappel, les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant. L'idée est d'avoir des ports d'émission séparés pour l'additionneur flottant et le multiplieur flottant. Le processeur peut alors émettre une addition flottante en même temps qu'une multiplication flottante. Les autres circuits de calcul flottant sont répartis sur ces deux ports d'émission flottants. L'avantage est que cela se marie bien avec l'usage d'une fenêtre d'instruction séparée pour les opérations flottantes. La fenêtre d'instruction a alors deux ports séparés, au lieu d'un seul. Rajouter un second port d'émission flottant n'est pas trop un problème, car le cout lié à l'ajout d'un port n'est pas linéaire. Passer de un port à deux a un cout tolérable, bien plus que de passer de 3 ports à 4 ou de 4 à 5. Il est possible de dupliquer l'additionneur et le multiplieur flottant, ce qui permet d'émettre deux additions et multiplications flottantes en même temps. C'est ce qui est fait sur les processeurs AMD d'architecture Zen 1 et 2. Ils ont deux additionneurs flottants par cœur, deux multiplieurs flottants, chacun avec leur propre port d'émission. Les performances en calcul flottant sont assez impressionnantes pour un processeur de l'époque. [[File:ZEN - émission multiple flottante.png|centre|vignette|upright=2|Microarchitecture Zen 1 d'AMD.]] ==Les ports d'émission et de décodage== Les ports d'émission et de décodage sont des ressources matérielles comme les autres. Dans cette section, nous allons parler de concepts liés aux ports d'émission et de décodage. Nous allons notamment parler du partage des ports d'émission, ainsi que des relations numériques entre ces deux types de ports. ===Le partage des ports d'émission et la ''port contention''=== Pour le moment, nous n'avons vu que des cas où un port d'émission est relié soit à des ALU entières, soit à des FPU, pas aux deux. Mais il existe des processeurs où un même port d'émission alimente à la fois une ALU entière et une FPU. Par exemple, on peut relier un additionneur flottant sur le même port qu'une ALU entière. Il faut noter que cela implique une fenêtre d'instruction centralisée, capable de mettre en attente micro-opérations entières et flottantes. Un exemple est celui des processeurs Core 2 Duo. Ils disposent de 6 ports d'émission, dont 3 ports dédiés à l'unité mémoire. Les 3 ports restants alimentent chacun : une ALU entière, un circuit de calcul flottant et une unité de calcul SSE (une unité de calcul SIMD, qu'on abordera dans quelques chapitres). * Le premier port alimente une ALU entière simple et un multiplieur/diviseur flottant. * Le second alimente une ALU entière, un multiplieur entier et un additionneur flottant. * Le troisième alimente une ALU entière, sans circuit flottant dédié. Dans le même genre, il est possible de partager un port d'émission entre l'unité mémoire et une ALU entière. Cela permet d'utiliser l'ALU entière pour les calculs d'adresse, ce qui évite d'avoir à utiliser une unité de calcul d'adresse distincte. Un exemple est celui du processeur superscalaire double émission Power PC 440. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. L'organisation en question permet soit d'émettre un accès mémoire en même temps qu'une opération entière, soit d'émettre deux opérations entières simples, soit d’émettre une multiplication et une addition/soustraction/comparaison. Une organisation simple, mais très efficace ! [[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]] ===La séparation entre largeur de décodage et d'émission=== Sur les processeurs superscalaires larges, le processeur peut émettre plus d'instructions qu'il peut en décoder. Plus haut, j'ai dit que l'unité d'émission avait parfois plus de ports d'émission que de ports de décodage. La raison est assez simple, et elle s'explique assez bien avec un exemple. Prenons un processeur avec trois ALU entières, une FPU, une unité de branchement et une unité mémoire. Chacune a son propre port d'émission, ce qui fait que le CPU est capable d'émettre 6 µops. Du moins, c'est la théorie, car les conditions pour sont assez rares. Il faut qu'en chargeant un paquet de 6 instructions, il y ait exactement trois instructions entières, une flottante, un branchement et un accès mémoire. Si le paquet contient 5 instructions entières et un branchement, seules quatre instructions sont émises. Dans cet exemple, dans la grande majorité des cas, seuls 4 ports seront utilisés en moyenne et deux seront sous-utilisés. Il s'agit là d'une moyenne, qui dépend du code, et des instructions exactes à exécuter. Cependant, cette moyenne nous donne un moyen d'économiser beaucoup de circuits pour un cout en performance mineur. L'idée est de ne charger que des blocs de 4 instructions, mais d'avoir 6 ports d'émission. Le processeur charge et décode 4 instructions, les quatre instructions sont envoyées sur les ports d'émission adéquat. Le cout en transistors est alors situé entre celui d'un CPU quadruple émission et sextuple émission. Et les performances sont aussi entre les deux. Pour les performances, c'est parce que cela permet d'avoir un CPU quadruple émission avec bien moins de contraintes d’appariement. Imaginez qu'on se soit limité à quatre ports d'émission : il aurait fallu mettre plusieurs unités sur le même port d'émission. En conséquence, il y aurait eu des combinaisons interdites, comme émettre deux instructions entières + un branchement + un accès mémoire ; ou deux instructions entières + une opération flottante + un accès mémoire. De tells appariement interdits sont possibles en utilisant les six ports d'émission. ==L'émission multiple des accès mémoire== Les premiers processeurs superscalaires ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles. Par contre, c'est autre chose sur les processeurs superscalaires larges. Vu qu'ils travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares. L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. La ''load/store queue'' reste une structure unique, mais on peut lui rajouter des ports. Et à ce peit jeu, il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire. La première duplique les unités de calcul, mais pas les ports d'accès au cache. Les accès mémoire sont alors accumulés dans la ''load/store queue'', qui sont ensuite traités un par un par le cache. Un processeur utilisant cette technique est, par exemple, capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. Un exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. Ces processeurs pouvaient calculer trois adresses simultanément, mais ne pouvaient faire qu'un seul accès au cache par cycle. Le cout en circuit est modéré : les unités de calcul coutent très peu, les ports de la ''load/store queue'' coutent déjà un peu plus. La seconde méthode ajoute des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultanées. Il faut alors dupliquer des unités de calcul, rajouter les ports sur le cache, mais aussi rajouter des ports sur la ''load/store queue'', pour qu'elle échange des données/commandes mémoire avec le cache. Le cout en circuits est alors conséquent Quelques processeurs utilisaient des solutions intermédiaires, un exemple classique étant les Pentium 1 et 2 d'Intel. Mais nous en parlerons dans le chapitre suivant. [[File:Processeur non-superscalaire basique 02.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]] Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre. Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Le Pentium 4 était dans ce cas, mais il était un peu seul dans sa catégorie. Un cas similaire est celui où l'unité mémoire est alimentée par une fenêtre de micro-opération mélangeant instructions mémoire et entières. Les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. Dans ce cas, l'émission multiple est rarement implémentée. Le cas suivant a une file pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique. Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui pouvaient seulement émettre une lecture en même temps qu'une écriture. Ils avaient deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse. [[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]] Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture demande d'utiliser une file d'écriture multiport, ce qui est assez compliqué. Implémenter des lectures en plus est plus simple : il suffit de rajouter une seconde file de µops LOAD et une unité de calcul, alimentées via un second port d'émission. Il est aussi possible de rajouter un second port de lecture au cache, mais ce n'est pas obligatoire. Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse. De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports de lecture et d'écriture car il y a plus de lecture que d'écritures. Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. : Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse. [[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]] Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettent plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail. Pour résumer, l'évolution des CPU superscalaires a été la suivante : * pipeline mémorie fusionné avec le pipeline entier ; * séparation du pipeline mémoire du reste, avec émission parallèle des accès mémoire ; * émission multiple, mais sans dupliquer les ports du cache ; * émission multiple, avec émission simultanée d'une lecture et d’une écriture dans le cache ; * émission multiple avec plusieurs accès en lecture au cache. ==Les optimisations des processeurs superscalaires larges== Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul. ===L'étape de chargement d'un CPU superscalaire large=== Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 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 autoaligné.]] Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements ! [[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]] Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez 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 microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total). Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois 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 microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement. Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations. ===La micro-fusion des processeurs Intel et AMD=== La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''. Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''. Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout. La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Le parallélisme mémoire | prevText=Le parallélisme mémoire | next=Exemples de microarchitectures CPU : le cas du x86 | nextText=Exemples de microarchitectures CPU : le cas du x86 }} </noinclude> demzhwoqrfjcuaa3ov9n4dvmoijg1xg 772700 772699 2026-09-21T17:21:24Z Mewtow 31375 /* L'unité d'émission d'un processeur superscalaire */ 772700 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 tout premiers processeurs superscalaires pouvaient émettre deux µops en même temps, ce qui portait le nom de '''double émission'''. Mais le nombre a rapidement augmenté et les processeurs modernes 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 des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. Il y a donc des paires d'instructions autorisées et des paires interdites. En pratique, ces contraintes d’appariement ont peu d’impact. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière, le ratio est le même quand on remplace la multiplication par un branchement ou un accès mémoire. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Deux branchements consécutifs sont quant à eux plus fréquents, idem pour deux accès mémoire. Dupliquer des ALU entières est donc la priorité, suivi par la duplication des unités de branchement, puis celle des accès mémoire, pour terminer par le multiplieur et la FPU. ==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. Par exemple, l'unité de chargement charge deux fois plus d'instructions, mais n'est pas modifiée en profondeur. L'unité d'émission et les circuits d'exécution dans le désordre sont eux aussi altérés. Les unités de calcul sont souvent dupliquées, sauf sur les tout premiers CPU superscalaires. {|class="wikitable" |- ! rowspan="2" | Processeur sans émission multiple | rowspan="2" | Chargement | rowspan="2" | Décodage | rowspan="2" | Renommage | rowspan="2" | Émission | Exécution / ALU | rowspan="2" | ''Commit''/ROB |- | Exécution / ALU |- ! rowspan="4" | Processeur superscalaire | rowspan="4" | Chargement | rowspan="2" | Décodage | rowspan="4" | Renommage | rowspan="4" | Émission | Exécution / ALU | rowspan="4" | ''Commit''/ROB |- | Exécution / ALU |- | rowspan="2" | Décodage | Exécution / ALU |- | Exécution / ALU |} Voyons ce qu'il en est dans le détail. ===Les unités de calcul d'un processeur superscalaire=== Émettre N instructions simultanément demande implicitement d'exécuter N instructions par cycle, voire plus si des instructions multicycles viennent mettre leur grain de sel. 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 basique, avec seulement des ALU entières, peut se le permettre. Du moins, à condition de ne pas dupliquer l'unité mémoire, ce qui fait qu'il a pas mal de contraintes d'appariement. Par exemple, un processeur RISC basique avec seulement une ALU entière et un ''barrel shifter'' peut être rendu superscalaire en dupliquant les deux circuits. Le problème, est que cette solution marche assez mal dès que le processeur contient des circuits multiplieurs et/ou une FPU. Par exemple, un processeur à double émission devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, etc. 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 ! Par contre, l'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission. Un autre cas extrême est celui où aucune unité de calcul n'est dupliquée. Pour comprendre comment c'est possible rappelez-vous qu'un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et éventuellement une unité de branchement. Cela suffit pour créer un processeur quadruple émission, très limité. Un tel processeur peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire. Le problème est qu'il est rare que l'on ait à exécuter en même temps un calcul entier, un calcul flottant, un accès mémoire et un branchement. La quadruple émission n'est donc que rarement utilisée. En pratique, les seules combinaisons fréquentes sont les combinaisons calcul + accès mémoire et calcul + branchement. Les combinaisons calcul entier + calcul flottant sont aussi possibles, mais plus rares. Mais on dépasse rarement la double émission. En clair, à partir de la quadruple émission, on ne peut plus se contenter d'une ALU, d'une FPU, d'une unité de branchement et d'une unité mémoire. Les processeurs superscalaires larges n'ont pas le choix que de rajouter des unités de calcul. Reste à voir ce qui est dupliqué en pratique. 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. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant. ===Le chargement de plusieurs instructions consécutives=== Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large. Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence. Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas. Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement non-pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc. [[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]] ===Le décodage parallèle des instructions=== Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits. Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur. Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! ===L'unité d'émission d'un processeur superscalaire=== La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. 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 4 instructions, compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément deux 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. 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. 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 microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle. 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=== Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement. 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. ==L'évolution historique des processeurs superscalaires== Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs triple émission permettent eux d’émettre trois instructions à la fois. Les tout premiers processeurs superscalaires avaient un budget en transistors très limité, ce qui fait qu'ils ne pouvaient pas être "totalement superscalaires". Par exemple, un processeur double émission ne pouvait pas émettre deux opérations entières en même temps, ni deux opérations flottantes, ni une opération entière avec un accès mémoire. Il y avait des paires d'instructions autorisées et des paires interdites. Les '''contraintes d'appariement''' sur les instructions à émettre en même temps étaient très strictes. L'intérêt de ces contraintes d’appariement est que le processeur était très simple, il n'y avait pas besoin de rajouter des circuits complexes pour rendre le processeur superscalaire. Les CPU superscalaires historiques ne faisaient que mieux utiliser des unités de calcul déjà présentes. Prenons un processeur non-superscalaire simple, illustré ci-dessous. Il contient : une unité de chargement, un décodeur, une unité d'émission, une ALU, une FPU, les bancs de registre entier et flottants, et une unité mémoire. Nous avons placé l'unité mémoire à la suite de l'ALU entière, car ce sera important pour les explications qui vont suivre. [[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]] Les processeurs superscalaires historiques modifiaient assez peu le schéma précédent. L'unité de chargement était adaptée pour lire plusieurs instructions depuis la mémoire, il y avait un seul décodeur, l'unité d'émission était assez peu modifiée. Mais une chose était certaine : aucune unité de calcul n'était dupliquée, le banc de registre n'était presque pas modifié. A la rigueur, gérer la triple 'émission avec les branchements demandait parfois d'ajouter des ports de lecture sur le banc de registre généraux, pas plus. ===La double émission entière-flottante=== Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est d'émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Le processeur a donc deux pipelines séparés : un pour les micro-opérations entières, un autre pour les micro-opérations flottantes. On parle alors de '''double émission entière-flottante'''. {|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, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité. La plupart des premiers processeurs superscalaires étaient de ce type. Les exemples les plus notables sont les processeurs POWER 1 et ses dérivés comme le ''RISC Single Chip''. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle. [[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]] La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisations visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate]. ===L'émission parallèle des branchements=== Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur. 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 modifier le banc de registres. Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. [[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]] Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications et les divisions ? Pour étudier ces questions, nous allons prendre le cas d'un processeur capable d’émettre trois opérations en même temps : deux opérations entières, et une flottante, potentiellement d'autres. {|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 |} 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|Emission multiple des opérations entières, implémentation courante]] Pour les branchements, deux solutions sont possibles. Avec la première, il y a émission parallèle des branchements, à savoir qu'il est possible d'émettre en branchement en parallèle de deux-trois instructions entières. Et cela impose la présence d'une unité de branchement séparée, avec son port d'émission. Avec la seconde, les branchements sont traités comme des instructions entières. Soit les branchements sont exécutés dans une ALU entière, soit il y a une unité de branchement reliée à un port d'émission pour les opérations entières. [[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Emission série et parallèle des branchements]] Avec la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales, sur les processeurs superscalaires historiques. En clair, le processeur peut émettre 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. De plus, 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. Ce compromis change cependant quand on passe à la quadruple émission. {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 |- | * ALU entière * Multiplieur | * ALU entière * Barrel shifter * Unité de branchement * Unité mémoire | FPU |- | * Additions, soustractions, opérations bit à bit, etc. * Multiplication | * Additions, soustractions, opérations bit à bit, etc. * Décalages et rotations * Branchements * Accès mémoire |Opération flottante |} ===Les CPU superscalaires à plusieurs unités spécialisées=== Pour la triple émission, deux opérations entières + une flottante est l'idéal. Même les processeurs modernes à triple émission restent sur cette "norme". Mais arrivé à la quadruple émission, les CPU superscalaires ont évolués dans des directions différentes, chacun faisant à sa sauce. La raison est que la quadruple émission permet de nombreuses possibilités, mais la plupart ne sont pas intéressantes. Prenons un CPU superscalaire avec une ALU entière, une FPU, une unité de branchement, et une unité mémoire. Il est possible de créer un CPU quadruple émission avec ces unités, mais le gain en performance n'en vaudrait pas la peine. Il est rare qu'un bloc de 4 instructions contienne exactement une opération entière, une flottante, un branchement et un accès mémoire. En pratique, les seules combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. En conséquence, une fois arrivé à la quadruple émission, les processeurs superscalaires n'ont pas le choix que de rajouter des unités de calcul, voire des unités mémoire. Et ils ont privilégié l'ajout d'ALU entières. La raison est que les programmes informatiques utilisent beaucoup plus d'additions/soustractions que le reste. Il y a environ 5 opérations entières de base pour une multiplication entière, le ratio est le même quand on remplace la multiplication par un branchement ou un accès mémoire. En conséquence, les ALU entières ont été dupliquées en premier, et dans de nombreux exemplaires. Reste à voir comment a été dupliqué le reste. Les multiplieurs n'ont pas été dupliqués, sauf sur les processeurs superscalaires très larges. La raison est qu'il est rare de devoir exécuter deux multiplications à la fois. Et c'est la même chose pour le ''barrel shifter''. Deux branchements consécutifs sont quant à eux plus fréquents, c'est même assez courant. Aussi, les unités de branchement ont été dupliquées en second lieu, après les ALU entières. Avoir plusieurs accès mémoire proches est assez courant, surtout sur les CPU de type CISC. Aussi, les unités mémoire ont été dupliquées en troisième lieu. Pour résumer, dupliquer des ALU entières est donc la priorité, suivi par la duplication des unités de branchement, puis celle des accès mémoire, pour terminer par le multiplieur et le ''barrel shifter''. La FPU est un peu à part. Une autre tendance a été de séparer l'unité mémoire des ALU entières, ce qui a permis l''''émission parallèle des µops mémoire'''. Comme son nom l'indique, l'idée est d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière. La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple. La '''double émission flottante''' a été utilisée sur les processeurs Alpha. L'idée est d'émettre deux opérations flottantes à la fois. Sauf qu'une implémentation naïve demanderait de dupliquer la FPU, ce qui aurait un cout en circuit rédhibitoire. Pour rappel, les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant. L'idée est d'avoir des ports d'émission séparés pour l'additionneur flottant et le multiplieur flottant. Le processeur peut alors émettre une addition flottante en même temps qu'une multiplication flottante. Les autres circuits de calcul flottant sont répartis sur ces deux ports d'émission flottants. L'avantage est que cela se marie bien avec l'usage d'une fenêtre d'instruction séparée pour les opérations flottantes. La fenêtre d'instruction a alors deux ports séparés, au lieu d'un seul. Rajouter un second port d'émission flottant n'est pas trop un problème, car le cout lié à l'ajout d'un port n'est pas linéaire. Passer de un port à deux a un cout tolérable, bien plus que de passer de 3 ports à 4 ou de 4 à 5. Il est possible de dupliquer l'additionneur et le multiplieur flottant, ce qui permet d'émettre deux additions et multiplications flottantes en même temps. C'est ce qui est fait sur les processeurs AMD d'architecture Zen 1 et 2. Ils ont deux additionneurs flottants par cœur, deux multiplieurs flottants, chacun avec leur propre port d'émission. Les performances en calcul flottant sont assez impressionnantes pour un processeur de l'époque. [[File:ZEN - émission multiple flottante.png|centre|vignette|upright=2|Microarchitecture Zen 1 d'AMD.]] ==Les ports d'émission et de décodage== Les ports d'émission et de décodage sont des ressources matérielles comme les autres. Dans cette section, nous allons parler de concepts liés aux ports d'émission et de décodage. Nous allons notamment parler du partage des ports d'émission, ainsi que des relations numériques entre ces deux types de ports. ===Le partage des ports d'émission et la ''port contention''=== Pour le moment, nous n'avons vu que des cas où un port d'émission est relié soit à des ALU entières, soit à des FPU, pas aux deux. Mais il existe des processeurs où un même port d'émission alimente à la fois une ALU entière et une FPU. Par exemple, on peut relier un additionneur flottant sur le même port qu'une ALU entière. Il faut noter que cela implique une fenêtre d'instruction centralisée, capable de mettre en attente micro-opérations entières et flottantes. Un exemple est celui des processeurs Core 2 Duo. Ils disposent de 6 ports d'émission, dont 3 ports dédiés à l'unité mémoire. Les 3 ports restants alimentent chacun : une ALU entière, un circuit de calcul flottant et une unité de calcul SSE (une unité de calcul SIMD, qu'on abordera dans quelques chapitres). * Le premier port alimente une ALU entière simple et un multiplieur/diviseur flottant. * Le second alimente une ALU entière, un multiplieur entier et un additionneur flottant. * Le troisième alimente une ALU entière, sans circuit flottant dédié. Dans le même genre, il est possible de partager un port d'émission entre l'unité mémoire et une ALU entière. Cela permet d'utiliser l'ALU entière pour les calculs d'adresse, ce qui évite d'avoir à utiliser une unité de calcul d'adresse distincte. Un exemple est celui du processeur superscalaire double émission Power PC 440. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. L'organisation en question permet soit d'émettre un accès mémoire en même temps qu'une opération entière, soit d'émettre deux opérations entières simples, soit d’émettre une multiplication et une addition/soustraction/comparaison. Une organisation simple, mais très efficace ! [[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]] ===La séparation entre largeur de décodage et d'émission=== Sur les processeurs superscalaires larges, le processeur peut émettre plus d'instructions qu'il peut en décoder. Plus haut, j'ai dit que l'unité d'émission avait parfois plus de ports d'émission que de ports de décodage. La raison est assez simple, et elle s'explique assez bien avec un exemple. Prenons un processeur avec trois ALU entières, une FPU, une unité de branchement et une unité mémoire. Chacune a son propre port d'émission, ce qui fait que le CPU est capable d'émettre 6 µops. Du moins, c'est la théorie, car les conditions pour sont assez rares. Il faut qu'en chargeant un paquet de 6 instructions, il y ait exactement trois instructions entières, une flottante, un branchement et un accès mémoire. Si le paquet contient 5 instructions entières et un branchement, seules quatre instructions sont émises. Dans cet exemple, dans la grande majorité des cas, seuls 4 ports seront utilisés en moyenne et deux seront sous-utilisés. Il s'agit là d'une moyenne, qui dépend du code, et des instructions exactes à exécuter. Cependant, cette moyenne nous donne un moyen d'économiser beaucoup de circuits pour un cout en performance mineur. L'idée est de ne charger que des blocs de 4 instructions, mais d'avoir 6 ports d'émission. Le processeur charge et décode 4 instructions, les quatre instructions sont envoyées sur les ports d'émission adéquat. Le cout en transistors est alors situé entre celui d'un CPU quadruple émission et sextuple émission. Et les performances sont aussi entre les deux. Pour les performances, c'est parce que cela permet d'avoir un CPU quadruple émission avec bien moins de contraintes d’appariement. Imaginez qu'on se soit limité à quatre ports d'émission : il aurait fallu mettre plusieurs unités sur le même port d'émission. En conséquence, il y aurait eu des combinaisons interdites, comme émettre deux instructions entières + un branchement + un accès mémoire ; ou deux instructions entières + une opération flottante + un accès mémoire. De tells appariement interdits sont possibles en utilisant les six ports d'émission. ==L'émission multiple des accès mémoire== Les premiers processeurs superscalaires ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles. Par contre, c'est autre chose sur les processeurs superscalaires larges. Vu qu'ils travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares. L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. La ''load/store queue'' reste une structure unique, mais on peut lui rajouter des ports. Et à ce peit jeu, il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire. La première duplique les unités de calcul, mais pas les ports d'accès au cache. Les accès mémoire sont alors accumulés dans la ''load/store queue'', qui sont ensuite traités un par un par le cache. Un processeur utilisant cette technique est, par exemple, capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. Un exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. Ces processeurs pouvaient calculer trois adresses simultanément, mais ne pouvaient faire qu'un seul accès au cache par cycle. Le cout en circuit est modéré : les unités de calcul coutent très peu, les ports de la ''load/store queue'' coutent déjà un peu plus. La seconde méthode ajoute des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultanées. Il faut alors dupliquer des unités de calcul, rajouter les ports sur le cache, mais aussi rajouter des ports sur la ''load/store queue'', pour qu'elle échange des données/commandes mémoire avec le cache. Le cout en circuits est alors conséquent Quelques processeurs utilisaient des solutions intermédiaires, un exemple classique étant les Pentium 1 et 2 d'Intel. Mais nous en parlerons dans le chapitre suivant. [[File:Processeur non-superscalaire basique 02.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]] Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre. Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Le Pentium 4 était dans ce cas, mais il était un peu seul dans sa catégorie. Un cas similaire est celui où l'unité mémoire est alimentée par une fenêtre de micro-opération mélangeant instructions mémoire et entières. Les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. Dans ce cas, l'émission multiple est rarement implémentée. Le cas suivant a une file pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique. Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui pouvaient seulement émettre une lecture en même temps qu'une écriture. Ils avaient deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse. [[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]] Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture demande d'utiliser une file d'écriture multiport, ce qui est assez compliqué. Implémenter des lectures en plus est plus simple : il suffit de rajouter une seconde file de µops LOAD et une unité de calcul, alimentées via un second port d'émission. Il est aussi possible de rajouter un second port de lecture au cache, mais ce n'est pas obligatoire. Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse. De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports de lecture et d'écriture car il y a plus de lecture que d'écritures. Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. : Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse. [[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]] Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettent plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail. Pour résumer, l'évolution des CPU superscalaires a été la suivante : * pipeline mémorie fusionné avec le pipeline entier ; * séparation du pipeline mémoire du reste, avec émission parallèle des accès mémoire ; * émission multiple, mais sans dupliquer les ports du cache ; * émission multiple, avec émission simultanée d'une lecture et d’une écriture dans le cache ; * émission multiple avec plusieurs accès en lecture au cache. ==Les optimisations des processeurs superscalaires larges== Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul. ===L'étape de chargement d'un CPU superscalaire large=== Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 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 autoaligné.]] Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements ! [[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]] Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez 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 microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total). Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois 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 microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement. Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations. ===La micro-fusion des processeurs Intel et AMD=== La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''. Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''. Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout. La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Le parallélisme mémoire | prevText=Le parallélisme mémoire | next=Exemples de microarchitectures CPU : le cas du x86 | nextText=Exemples de microarchitectures CPU : le cas du x86 }} </noinclude> 4uel3n2bugvvbva4zgeb6vj73n5eroq 772702 772700 2026-09-21T17:24:14Z Mewtow 31375 /* L'émission multiple des accès mémoire */ 772702 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 tout premiers processeurs superscalaires pouvaient émettre deux µops en même temps, ce qui portait le nom de '''double émission'''. Mais le nombre a rapidement augmenté et les processeurs modernes 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 des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. Il y a donc des paires d'instructions autorisées et des paires interdites. En pratique, ces contraintes d’appariement ont peu d’impact. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière, le ratio est le même quand on remplace la multiplication par un branchement ou un accès mémoire. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Deux branchements consécutifs sont quant à eux plus fréquents, idem pour deux accès mémoire. Dupliquer des ALU entières est donc la priorité, suivi par la duplication des unités de branchement, puis celle des accès mémoire, pour terminer par le multiplieur et la FPU. ==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. Par exemple, l'unité de chargement charge deux fois plus d'instructions, mais n'est pas modifiée en profondeur. L'unité d'émission et les circuits d'exécution dans le désordre sont eux aussi altérés. Les unités de calcul sont souvent dupliquées, sauf sur les tout premiers CPU superscalaires. {|class="wikitable" |- ! rowspan="2" | Processeur sans émission multiple | rowspan="2" | Chargement | rowspan="2" | Décodage | rowspan="2" | Renommage | rowspan="2" | Émission | Exécution / ALU | rowspan="2" | ''Commit''/ROB |- | Exécution / ALU |- ! rowspan="4" | Processeur superscalaire | rowspan="4" | Chargement | rowspan="2" | Décodage | rowspan="4" | Renommage | rowspan="4" | Émission | Exécution / ALU | rowspan="4" | ''Commit''/ROB |- | Exécution / ALU |- | rowspan="2" | Décodage | Exécution / ALU |- | Exécution / ALU |} Voyons ce qu'il en est dans le détail. ===Les unités de calcul d'un processeur superscalaire=== Émettre N instructions simultanément demande implicitement d'exécuter N instructions par cycle, voire plus si des instructions multicycles viennent mettre leur grain de sel. 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 basique, avec seulement des ALU entières, peut se le permettre. Du moins, à condition de ne pas dupliquer l'unité mémoire, ce qui fait qu'il a pas mal de contraintes d'appariement. Par exemple, un processeur RISC basique avec seulement une ALU entière et un ''barrel shifter'' peut être rendu superscalaire en dupliquant les deux circuits. Le problème, est que cette solution marche assez mal dès que le processeur contient des circuits multiplieurs et/ou une FPU. Par exemple, un processeur à double émission devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, etc. 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 ! Par contre, l'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission. Un autre cas extrême est celui où aucune unité de calcul n'est dupliquée. Pour comprendre comment c'est possible rappelez-vous qu'un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et éventuellement une unité de branchement. Cela suffit pour créer un processeur quadruple émission, très limité. Un tel processeur peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire. Le problème est qu'il est rare que l'on ait à exécuter en même temps un calcul entier, un calcul flottant, un accès mémoire et un branchement. La quadruple émission n'est donc que rarement utilisée. En pratique, les seules combinaisons fréquentes sont les combinaisons calcul + accès mémoire et calcul + branchement. Les combinaisons calcul entier + calcul flottant sont aussi possibles, mais plus rares. Mais on dépasse rarement la double émission. En clair, à partir de la quadruple émission, on ne peut plus se contenter d'une ALU, d'une FPU, d'une unité de branchement et d'une unité mémoire. Les processeurs superscalaires larges n'ont pas le choix que de rajouter des unités de calcul. Reste à voir ce qui est dupliqué en pratique. 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. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant. ===Le chargement de plusieurs instructions consécutives=== Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large. Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence. Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas. Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement non-pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc. [[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]] ===Le décodage parallèle des instructions=== Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits. Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur. Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! ===L'unité d'émission d'un processeur superscalaire=== La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. 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 4 instructions, compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément deux 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. 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. 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 microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle. 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=== Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement. 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. ==L'évolution historique des processeurs superscalaires== Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs triple émission permettent eux d’émettre trois instructions à la fois. Les tout premiers processeurs superscalaires avaient un budget en transistors très limité, ce qui fait qu'ils ne pouvaient pas être "totalement superscalaires". Par exemple, un processeur double émission ne pouvait pas émettre deux opérations entières en même temps, ni deux opérations flottantes, ni une opération entière avec un accès mémoire. Il y avait des paires d'instructions autorisées et des paires interdites. Les '''contraintes d'appariement''' sur les instructions à émettre en même temps étaient très strictes. L'intérêt de ces contraintes d’appariement est que le processeur était très simple, il n'y avait pas besoin de rajouter des circuits complexes pour rendre le processeur superscalaire. Les CPU superscalaires historiques ne faisaient que mieux utiliser des unités de calcul déjà présentes. Prenons un processeur non-superscalaire simple, illustré ci-dessous. Il contient : une unité de chargement, un décodeur, une unité d'émission, une ALU, une FPU, les bancs de registre entier et flottants, et une unité mémoire. Nous avons placé l'unité mémoire à la suite de l'ALU entière, car ce sera important pour les explications qui vont suivre. [[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]] Les processeurs superscalaires historiques modifiaient assez peu le schéma précédent. L'unité de chargement était adaptée pour lire plusieurs instructions depuis la mémoire, il y avait un seul décodeur, l'unité d'émission était assez peu modifiée. Mais une chose était certaine : aucune unité de calcul n'était dupliquée, le banc de registre n'était presque pas modifié. A la rigueur, gérer la triple 'émission avec les branchements demandait parfois d'ajouter des ports de lecture sur le banc de registre généraux, pas plus. ===La double émission entière-flottante=== Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est d'émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Le processeur a donc deux pipelines séparés : un pour les micro-opérations entières, un autre pour les micro-opérations flottantes. On parle alors de '''double émission entière-flottante'''. {|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, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité. La plupart des premiers processeurs superscalaires étaient de ce type. Les exemples les plus notables sont les processeurs POWER 1 et ses dérivés comme le ''RISC Single Chip''. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle. [[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]] La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisations visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate]. ===L'émission parallèle des branchements=== Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur. 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 modifier le banc de registres. Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. [[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]] Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications et les divisions ? Pour étudier ces questions, nous allons prendre le cas d'un processeur capable d’émettre trois opérations en même temps : deux opérations entières, et une flottante, potentiellement d'autres. {|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 |} 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|Emission multiple des opérations entières, implémentation courante]] Pour les branchements, deux solutions sont possibles. Avec la première, il y a émission parallèle des branchements, à savoir qu'il est possible d'émettre en branchement en parallèle de deux-trois instructions entières. Et cela impose la présence d'une unité de branchement séparée, avec son port d'émission. Avec la seconde, les branchements sont traités comme des instructions entières. Soit les branchements sont exécutés dans une ALU entière, soit il y a une unité de branchement reliée à un port d'émission pour les opérations entières. [[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Emission série et parallèle des branchements]] Avec la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales, sur les processeurs superscalaires historiques. En clair, le processeur peut émettre 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. De plus, 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. Ce compromis change cependant quand on passe à la quadruple émission. {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 |- | * ALU entière * Multiplieur | * ALU entière * Barrel shifter * Unité de branchement * Unité mémoire | FPU |- | * Additions, soustractions, opérations bit à bit, etc. * Multiplication | * Additions, soustractions, opérations bit à bit, etc. * Décalages et rotations * Branchements * Accès mémoire |Opération flottante |} ===Les CPU superscalaires à plusieurs unités spécialisées=== Pour la triple émission, deux opérations entières + une flottante est l'idéal. Même les processeurs modernes à triple émission restent sur cette "norme". Mais arrivé à la quadruple émission, les CPU superscalaires ont évolués dans des directions différentes, chacun faisant à sa sauce. La raison est que la quadruple émission permet de nombreuses possibilités, mais la plupart ne sont pas intéressantes. Prenons un CPU superscalaire avec une ALU entière, une FPU, une unité de branchement, et une unité mémoire. Il est possible de créer un CPU quadruple émission avec ces unités, mais le gain en performance n'en vaudrait pas la peine. Il est rare qu'un bloc de 4 instructions contienne exactement une opération entière, une flottante, un branchement et un accès mémoire. En pratique, les seules combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. En conséquence, une fois arrivé à la quadruple émission, les processeurs superscalaires n'ont pas le choix que de rajouter des unités de calcul, voire des unités mémoire. Et ils ont privilégié l'ajout d'ALU entières. La raison est que les programmes informatiques utilisent beaucoup plus d'additions/soustractions que le reste. Il y a environ 5 opérations entières de base pour une multiplication entière, le ratio est le même quand on remplace la multiplication par un branchement ou un accès mémoire. En conséquence, les ALU entières ont été dupliquées en premier, et dans de nombreux exemplaires. Reste à voir comment a été dupliqué le reste. Les multiplieurs n'ont pas été dupliqués, sauf sur les processeurs superscalaires très larges. La raison est qu'il est rare de devoir exécuter deux multiplications à la fois. Et c'est la même chose pour le ''barrel shifter''. Deux branchements consécutifs sont quant à eux plus fréquents, c'est même assez courant. Aussi, les unités de branchement ont été dupliquées en second lieu, après les ALU entières. Avoir plusieurs accès mémoire proches est assez courant, surtout sur les CPU de type CISC. Aussi, les unités mémoire ont été dupliquées en troisième lieu. Pour résumer, dupliquer des ALU entières est donc la priorité, suivi par la duplication des unités de branchement, puis celle des accès mémoire, pour terminer par le multiplieur et le ''barrel shifter''. La FPU est un peu à part. Une autre tendance a été de séparer l'unité mémoire des ALU entières, ce qui a permis l''''émission parallèle des µops mémoire'''. Comme son nom l'indique, l'idée est d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière. La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple. La '''double émission flottante''' a été utilisée sur les processeurs Alpha. L'idée est d'émettre deux opérations flottantes à la fois. Sauf qu'une implémentation naïve demanderait de dupliquer la FPU, ce qui aurait un cout en circuit rédhibitoire. Pour rappel, les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant. L'idée est d'avoir des ports d'émission séparés pour l'additionneur flottant et le multiplieur flottant. Le processeur peut alors émettre une addition flottante en même temps qu'une multiplication flottante. Les autres circuits de calcul flottant sont répartis sur ces deux ports d'émission flottants. L'avantage est que cela se marie bien avec l'usage d'une fenêtre d'instruction séparée pour les opérations flottantes. La fenêtre d'instruction a alors deux ports séparés, au lieu d'un seul. Rajouter un second port d'émission flottant n'est pas trop un problème, car le cout lié à l'ajout d'un port n'est pas linéaire. Passer de un port à deux a un cout tolérable, bien plus que de passer de 3 ports à 4 ou de 4 à 5. Il est possible de dupliquer l'additionneur et le multiplieur flottant, ce qui permet d'émettre deux additions et multiplications flottantes en même temps. C'est ce qui est fait sur les processeurs AMD d'architecture Zen 1 et 2. Ils ont deux additionneurs flottants par cœur, deux multiplieurs flottants, chacun avec leur propre port d'émission. Les performances en calcul flottant sont assez impressionnantes pour un processeur de l'époque. [[File:ZEN - émission multiple flottante.png|centre|vignette|upright=2|Microarchitecture Zen 1 d'AMD.]] ==Les ports d'émission et de décodage== Les ports d'émission et de décodage sont des ressources matérielles comme les autres. Dans cette section, nous allons parler de concepts liés aux ports d'émission et de décodage. Nous allons notamment parler du partage des ports d'émission, ainsi que des relations numériques entre ces deux types de ports. ===Le partage des ports d'émission et la ''port contention''=== Pour le moment, nous n'avons vu que des cas où un port d'émission est relié soit à des ALU entières, soit à des FPU, pas aux deux. Mais il existe des processeurs où un même port d'émission alimente à la fois une ALU entière et une FPU. Par exemple, on peut relier un additionneur flottant sur le même port qu'une ALU entière. Il faut noter que cela implique une fenêtre d'instruction centralisée, capable de mettre en attente micro-opérations entières et flottantes. Un exemple est celui des processeurs Core 2 Duo. Ils disposent de 6 ports d'émission, dont 3 ports dédiés à l'unité mémoire. Les 3 ports restants alimentent chacun : une ALU entière, un circuit de calcul flottant et une unité de calcul SSE (une unité de calcul SIMD, qu'on abordera dans quelques chapitres). * Le premier port alimente une ALU entière simple et un multiplieur/diviseur flottant. * Le second alimente une ALU entière, un multiplieur entier et un additionneur flottant. * Le troisième alimente une ALU entière, sans circuit flottant dédié. Dans le même genre, il est possible de partager un port d'émission entre l'unité mémoire et une ALU entière. Cela permet d'utiliser l'ALU entière pour les calculs d'adresse, ce qui évite d'avoir à utiliser une unité de calcul d'adresse distincte. Un exemple est celui du processeur superscalaire double émission Power PC 440. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. L'organisation en question permet soit d'émettre un accès mémoire en même temps qu'une opération entière, soit d'émettre deux opérations entières simples, soit d’émettre une multiplication et une addition/soustraction/comparaison. Une organisation simple, mais très efficace ! [[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]] ===La séparation entre largeur de décodage et d'émission=== Sur les processeurs superscalaires larges, le processeur peut émettre plus d'instructions qu'il peut en décoder. Plus haut, j'ai dit que l'unité d'émission avait parfois plus de ports d'émission que de ports de décodage. La raison est assez simple, et elle s'explique assez bien avec un exemple. Prenons un processeur avec trois ALU entières, une FPU, une unité de branchement et une unité mémoire. Chacune a son propre port d'émission, ce qui fait que le CPU est capable d'émettre 6 µops. Du moins, c'est la théorie, car les conditions pour sont assez rares. Il faut qu'en chargeant un paquet de 6 instructions, il y ait exactement trois instructions entières, une flottante, un branchement et un accès mémoire. Si le paquet contient 5 instructions entières et un branchement, seules quatre instructions sont émises. Dans cet exemple, dans la grande majorité des cas, seuls 4 ports seront utilisés en moyenne et deux seront sous-utilisés. Il s'agit là d'une moyenne, qui dépend du code, et des instructions exactes à exécuter. Cependant, cette moyenne nous donne un moyen d'économiser beaucoup de circuits pour un cout en performance mineur. L'idée est de ne charger que des blocs de 4 instructions, mais d'avoir 6 ports d'émission. Le processeur charge et décode 4 instructions, les quatre instructions sont envoyées sur les ports d'émission adéquat. Le cout en transistors est alors situé entre celui d'un CPU quadruple émission et sextuple émission. Et les performances sont aussi entre les deux. Pour les performances, c'est parce que cela permet d'avoir un CPU quadruple émission avec bien moins de contraintes d’appariement. Imaginez qu'on se soit limité à quatre ports d'émission : il aurait fallu mettre plusieurs unités sur le même port d'émission. En conséquence, il y aurait eu des combinaisons interdites, comme émettre deux instructions entières + un branchement + un accès mémoire ; ou deux instructions entières + une opération flottante + un accès mémoire. De tells appariement interdits sont possibles en utilisant les six ports d'émission. ==L'émission multiple des accès mémoire== Les premiers processeurs superscalaires ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles. Par contre, c'est autre chose sur les processeurs superscalaires larges. Vu qu'ils travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares. L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. La ''load/store queue'' reste une structure unique, mais on peut lui rajouter des ports. Et à ce peit jeu, il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire. La première duplique les unités de calcul, mais pas les ports d'accès au cache. Les accès mémoire sont alors accumulés dans la ''load/store queue'', qui sont ensuite traités un par un par le cache. Un processeur utilisant cette technique est, par exemple, capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. Un exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. Ces processeurs pouvaient calculer trois adresses simultanément, mais ne pouvaient faire qu'un seul accès au cache par cycle. Le cout en circuit est modéré : les unités de calcul coutent très peu, les ports de la ''load/store queue'' coutent déjà un peu plus. La seconde méthode ajoute des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultanées. Il faut alors dupliquer des unités de calcul, rajouter les ports sur le cache, mais aussi rajouter des ports sur la ''load/store queue'', pour qu'elle échange des données/commandes mémoire avec le cache. Le cout en circuits est alors conséquent Quelques processeurs utilisaient des solutions intermédiaires, un exemple classique étant les Pentium 1 et 2 d'Intel. Mais nous en parlerons dans le chapitre suivant. [[File:Processeur non-superscalaire basique 02.png|centre|vignette|upright=1|Processeur non-superscalaire basique]] Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre. Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Le Pentium 4 était dans ce cas, mais il était un peu seul dans sa catégorie. Un cas similaire est celui où l'unité mémoire est alimentée par une fenêtre de micro-opération mélangeant instructions mémoire et entières. Les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. Dans ce cas, l'émission multiple est rarement implémentée. Le cas suivant a une file pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique. Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui pouvaient seulement émettre une lecture en même temps qu'une écriture. Ils avaient deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse. [[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]] Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture demande d'utiliser une file d'écriture multiport, ce qui est assez compliqué. Implémenter des lectures en plus est plus simple : il suffit de rajouter une seconde file de µops LOAD et une unité de calcul, alimentées via un second port d'émission. Il est aussi possible de rajouter un second port de lecture au cache, mais ce n'est pas obligatoire. Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse. De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports de lecture et d'écriture car il y a plus de lecture que d'écritures. Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. : Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse. [[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]] Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettent plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail. Pour résumer, l'évolution des CPU superscalaires a été la suivante : * pipeline mémorie fusionné avec le pipeline entier ; * séparation du pipeline mémoire du reste, avec émission parallèle des accès mémoire ; * émission multiple, mais sans dupliquer les ports du cache ; * émission multiple, avec émission simultanée d'une lecture et d’une écriture dans le cache ; * émission multiple avec plusieurs accès en lecture au cache. ==Les optimisations des processeurs superscalaires larges== Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul. ===L'étape de chargement d'un CPU superscalaire large=== Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 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 autoaligné.]] Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements ! [[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]] Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez 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 microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total). Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois 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 microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement. Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations. ===La micro-fusion des processeurs Intel et AMD=== La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''. Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''. Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout. La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Le parallélisme mémoire | prevText=Le parallélisme mémoire | next=Exemples de microarchitectures CPU : le cas du x86 | nextText=Exemples de microarchitectures CPU : le cas du x86 }} </noinclude> 56seynzzvzhcfjrd8326lldq9sc5gew 772703 772702 2026-09-21T17:24:43Z Mewtow 31375 /* L'émission multiple des accès mémoire */ 772703 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 tout premiers processeurs superscalaires pouvaient émettre deux µops en même temps, ce qui portait le nom de '''double émission'''. Mais le nombre a rapidement augmenté et les processeurs modernes 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 des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. Il y a donc des paires d'instructions autorisées et des paires interdites. En pratique, ces contraintes d’appariement ont peu d’impact. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière, le ratio est le même quand on remplace la multiplication par un branchement ou un accès mémoire. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Deux branchements consécutifs sont quant à eux plus fréquents, idem pour deux accès mémoire. Dupliquer des ALU entières est donc la priorité, suivi par la duplication des unités de branchement, puis celle des accès mémoire, pour terminer par le multiplieur et la FPU. ==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. Par exemple, l'unité de chargement charge deux fois plus d'instructions, mais n'est pas modifiée en profondeur. L'unité d'émission et les circuits d'exécution dans le désordre sont eux aussi altérés. Les unités de calcul sont souvent dupliquées, sauf sur les tout premiers CPU superscalaires. {|class="wikitable" |- ! rowspan="2" | Processeur sans émission multiple | rowspan="2" | Chargement | rowspan="2" | Décodage | rowspan="2" | Renommage | rowspan="2" | Émission | Exécution / ALU | rowspan="2" | ''Commit''/ROB |- | Exécution / ALU |- ! rowspan="4" | Processeur superscalaire | rowspan="4" | Chargement | rowspan="2" | Décodage | rowspan="4" | Renommage | rowspan="4" | Émission | Exécution / ALU | rowspan="4" | ''Commit''/ROB |- | Exécution / ALU |- | rowspan="2" | Décodage | Exécution / ALU |- | Exécution / ALU |} Voyons ce qu'il en est dans le détail. ===Les unités de calcul d'un processeur superscalaire=== Émettre N instructions simultanément demande implicitement d'exécuter N instructions par cycle, voire plus si des instructions multicycles viennent mettre leur grain de sel. 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 basique, avec seulement des ALU entières, peut se le permettre. Du moins, à condition de ne pas dupliquer l'unité mémoire, ce qui fait qu'il a pas mal de contraintes d'appariement. Par exemple, un processeur RISC basique avec seulement une ALU entière et un ''barrel shifter'' peut être rendu superscalaire en dupliquant les deux circuits. Le problème, est que cette solution marche assez mal dès que le processeur contient des circuits multiplieurs et/ou une FPU. Par exemple, un processeur à double émission devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, etc. 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 ! Par contre, l'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission. Un autre cas extrême est celui où aucune unité de calcul n'est dupliquée. Pour comprendre comment c'est possible rappelez-vous qu'un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et éventuellement une unité de branchement. Cela suffit pour créer un processeur quadruple émission, très limité. Un tel processeur peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire. Le problème est qu'il est rare que l'on ait à exécuter en même temps un calcul entier, un calcul flottant, un accès mémoire et un branchement. La quadruple émission n'est donc que rarement utilisée. En pratique, les seules combinaisons fréquentes sont les combinaisons calcul + accès mémoire et calcul + branchement. Les combinaisons calcul entier + calcul flottant sont aussi possibles, mais plus rares. Mais on dépasse rarement la double émission. En clair, à partir de la quadruple émission, on ne peut plus se contenter d'une ALU, d'une FPU, d'une unité de branchement et d'une unité mémoire. Les processeurs superscalaires larges n'ont pas le choix que de rajouter des unités de calcul. Reste à voir ce qui est dupliqué en pratique. 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. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant. ===Le chargement de plusieurs instructions consécutives=== Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large. Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence. Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas. Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement non-pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc. [[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]] ===Le décodage parallèle des instructions=== Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits. Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur. Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! ===L'unité d'émission d'un processeur superscalaire=== La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. 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 4 instructions, compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément deux 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. 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. 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 microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle. 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=== Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement. 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. ==L'évolution historique des processeurs superscalaires== Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs triple émission permettent eux d’émettre trois instructions à la fois. Les tout premiers processeurs superscalaires avaient un budget en transistors très limité, ce qui fait qu'ils ne pouvaient pas être "totalement superscalaires". Par exemple, un processeur double émission ne pouvait pas émettre deux opérations entières en même temps, ni deux opérations flottantes, ni une opération entière avec un accès mémoire. Il y avait des paires d'instructions autorisées et des paires interdites. Les '''contraintes d'appariement''' sur les instructions à émettre en même temps étaient très strictes. L'intérêt de ces contraintes d’appariement est que le processeur était très simple, il n'y avait pas besoin de rajouter des circuits complexes pour rendre le processeur superscalaire. Les CPU superscalaires historiques ne faisaient que mieux utiliser des unités de calcul déjà présentes. Prenons un processeur non-superscalaire simple, illustré ci-dessous. Il contient : une unité de chargement, un décodeur, une unité d'émission, une ALU, une FPU, les bancs de registre entier et flottants, et une unité mémoire. Nous avons placé l'unité mémoire à la suite de l'ALU entière, car ce sera important pour les explications qui vont suivre. [[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]] Les processeurs superscalaires historiques modifiaient assez peu le schéma précédent. L'unité de chargement était adaptée pour lire plusieurs instructions depuis la mémoire, il y avait un seul décodeur, l'unité d'émission était assez peu modifiée. Mais une chose était certaine : aucune unité de calcul n'était dupliquée, le banc de registre n'était presque pas modifié. A la rigueur, gérer la triple 'émission avec les branchements demandait parfois d'ajouter des ports de lecture sur le banc de registre généraux, pas plus. ===La double émission entière-flottante=== Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est d'émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Le processeur a donc deux pipelines séparés : un pour les micro-opérations entières, un autre pour les micro-opérations flottantes. On parle alors de '''double émission entière-flottante'''. {|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, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité. La plupart des premiers processeurs superscalaires étaient de ce type. Les exemples les plus notables sont les processeurs POWER 1 et ses dérivés comme le ''RISC Single Chip''. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle. [[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]] La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisations visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate]. ===L'émission parallèle des branchements=== Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur. 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 modifier le banc de registres. Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. [[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]] Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications et les divisions ? Pour étudier ces questions, nous allons prendre le cas d'un processeur capable d’émettre trois opérations en même temps : deux opérations entières, et une flottante, potentiellement d'autres. {|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 |} 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|Emission multiple des opérations entières, implémentation courante]] Pour les branchements, deux solutions sont possibles. Avec la première, il y a émission parallèle des branchements, à savoir qu'il est possible d'émettre en branchement en parallèle de deux-trois instructions entières. Et cela impose la présence d'une unité de branchement séparée, avec son port d'émission. Avec la seconde, les branchements sont traités comme des instructions entières. Soit les branchements sont exécutés dans une ALU entière, soit il y a une unité de branchement reliée à un port d'émission pour les opérations entières. [[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Emission série et parallèle des branchements]] Avec la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales, sur les processeurs superscalaires historiques. En clair, le processeur peut émettre 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. De plus, 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. Ce compromis change cependant quand on passe à la quadruple émission. {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 |- | * ALU entière * Multiplieur | * ALU entière * Barrel shifter * Unité de branchement * Unité mémoire | FPU |- | * Additions, soustractions, opérations bit à bit, etc. * Multiplication | * Additions, soustractions, opérations bit à bit, etc. * Décalages et rotations * Branchements * Accès mémoire |Opération flottante |} ===Les CPU superscalaires à plusieurs unités spécialisées=== Pour la triple émission, deux opérations entières + une flottante est l'idéal. Même les processeurs modernes à triple émission restent sur cette "norme". Mais arrivé à la quadruple émission, les CPU superscalaires ont évolués dans des directions différentes, chacun faisant à sa sauce. La raison est que la quadruple émission permet de nombreuses possibilités, mais la plupart ne sont pas intéressantes. Prenons un CPU superscalaire avec une ALU entière, une FPU, une unité de branchement, et une unité mémoire. Il est possible de créer un CPU quadruple émission avec ces unités, mais le gain en performance n'en vaudrait pas la peine. Il est rare qu'un bloc de 4 instructions contienne exactement une opération entière, une flottante, un branchement et un accès mémoire. En pratique, les seules combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. En conséquence, une fois arrivé à la quadruple émission, les processeurs superscalaires n'ont pas le choix que de rajouter des unités de calcul, voire des unités mémoire. Et ils ont privilégié l'ajout d'ALU entières. La raison est que les programmes informatiques utilisent beaucoup plus d'additions/soustractions que le reste. Il y a environ 5 opérations entières de base pour une multiplication entière, le ratio est le même quand on remplace la multiplication par un branchement ou un accès mémoire. En conséquence, les ALU entières ont été dupliquées en premier, et dans de nombreux exemplaires. Reste à voir comment a été dupliqué le reste. Les multiplieurs n'ont pas été dupliqués, sauf sur les processeurs superscalaires très larges. La raison est qu'il est rare de devoir exécuter deux multiplications à la fois. Et c'est la même chose pour le ''barrel shifter''. Deux branchements consécutifs sont quant à eux plus fréquents, c'est même assez courant. Aussi, les unités de branchement ont été dupliquées en second lieu, après les ALU entières. Avoir plusieurs accès mémoire proches est assez courant, surtout sur les CPU de type CISC. Aussi, les unités mémoire ont été dupliquées en troisième lieu. Pour résumer, dupliquer des ALU entières est donc la priorité, suivi par la duplication des unités de branchement, puis celle des accès mémoire, pour terminer par le multiplieur et le ''barrel shifter''. La FPU est un peu à part. Une autre tendance a été de séparer l'unité mémoire des ALU entières, ce qui a permis l''''émission parallèle des µops mémoire'''. Comme son nom l'indique, l'idée est d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière. La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple. La '''double émission flottante''' a été utilisée sur les processeurs Alpha. L'idée est d'émettre deux opérations flottantes à la fois. Sauf qu'une implémentation naïve demanderait de dupliquer la FPU, ce qui aurait un cout en circuit rédhibitoire. Pour rappel, les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant. L'idée est d'avoir des ports d'émission séparés pour l'additionneur flottant et le multiplieur flottant. Le processeur peut alors émettre une addition flottante en même temps qu'une multiplication flottante. Les autres circuits de calcul flottant sont répartis sur ces deux ports d'émission flottants. L'avantage est que cela se marie bien avec l'usage d'une fenêtre d'instruction séparée pour les opérations flottantes. La fenêtre d'instruction a alors deux ports séparés, au lieu d'un seul. Rajouter un second port d'émission flottant n'est pas trop un problème, car le cout lié à l'ajout d'un port n'est pas linéaire. Passer de un port à deux a un cout tolérable, bien plus que de passer de 3 ports à 4 ou de 4 à 5. Il est possible de dupliquer l'additionneur et le multiplieur flottant, ce qui permet d'émettre deux additions et multiplications flottantes en même temps. C'est ce qui est fait sur les processeurs AMD d'architecture Zen 1 et 2. Ils ont deux additionneurs flottants par cœur, deux multiplieurs flottants, chacun avec leur propre port d'émission. Les performances en calcul flottant sont assez impressionnantes pour un processeur de l'époque. [[File:ZEN - émission multiple flottante.png|centre|vignette|upright=2|Microarchitecture Zen 1 d'AMD.]] ==Les ports d'émission et de décodage== Les ports d'émission et de décodage sont des ressources matérielles comme les autres. Dans cette section, nous allons parler de concepts liés aux ports d'émission et de décodage. Nous allons notamment parler du partage des ports d'émission, ainsi que des relations numériques entre ces deux types de ports. ===Le partage des ports d'émission et la ''port contention''=== Pour le moment, nous n'avons vu que des cas où un port d'émission est relié soit à des ALU entières, soit à des FPU, pas aux deux. Mais il existe des processeurs où un même port d'émission alimente à la fois une ALU entière et une FPU. Par exemple, on peut relier un additionneur flottant sur le même port qu'une ALU entière. Il faut noter que cela implique une fenêtre d'instruction centralisée, capable de mettre en attente micro-opérations entières et flottantes. Un exemple est celui des processeurs Core 2 Duo. Ils disposent de 6 ports d'émission, dont 3 ports dédiés à l'unité mémoire. Les 3 ports restants alimentent chacun : une ALU entière, un circuit de calcul flottant et une unité de calcul SSE (une unité de calcul SIMD, qu'on abordera dans quelques chapitres). * Le premier port alimente une ALU entière simple et un multiplieur/diviseur flottant. * Le second alimente une ALU entière, un multiplieur entier et un additionneur flottant. * Le troisième alimente une ALU entière, sans circuit flottant dédié. Dans le même genre, il est possible de partager un port d'émission entre l'unité mémoire et une ALU entière. Cela permet d'utiliser l'ALU entière pour les calculs d'adresse, ce qui évite d'avoir à utiliser une unité de calcul d'adresse distincte. Un exemple est celui du processeur superscalaire double émission Power PC 440. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. L'organisation en question permet soit d'émettre un accès mémoire en même temps qu'une opération entière, soit d'émettre deux opérations entières simples, soit d’émettre une multiplication et une addition/soustraction/comparaison. Une organisation simple, mais très efficace ! [[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]] ===La séparation entre largeur de décodage et d'émission=== Sur les processeurs superscalaires larges, le processeur peut émettre plus d'instructions qu'il peut en décoder. Plus haut, j'ai dit que l'unité d'émission avait parfois plus de ports d'émission que de ports de décodage. La raison est assez simple, et elle s'explique assez bien avec un exemple. Prenons un processeur avec trois ALU entières, une FPU, une unité de branchement et une unité mémoire. Chacune a son propre port d'émission, ce qui fait que le CPU est capable d'émettre 6 µops. Du moins, c'est la théorie, car les conditions pour sont assez rares. Il faut qu'en chargeant un paquet de 6 instructions, il y ait exactement trois instructions entières, une flottante, un branchement et un accès mémoire. Si le paquet contient 5 instructions entières et un branchement, seules quatre instructions sont émises. Dans cet exemple, dans la grande majorité des cas, seuls 4 ports seront utilisés en moyenne et deux seront sous-utilisés. Il s'agit là d'une moyenne, qui dépend du code, et des instructions exactes à exécuter. Cependant, cette moyenne nous donne un moyen d'économiser beaucoup de circuits pour un cout en performance mineur. L'idée est de ne charger que des blocs de 4 instructions, mais d'avoir 6 ports d'émission. Le processeur charge et décode 4 instructions, les quatre instructions sont envoyées sur les ports d'émission adéquat. Le cout en transistors est alors situé entre celui d'un CPU quadruple émission et sextuple émission. Et les performances sont aussi entre les deux. Pour les performances, c'est parce que cela permet d'avoir un CPU quadruple émission avec bien moins de contraintes d’appariement. Imaginez qu'on se soit limité à quatre ports d'émission : il aurait fallu mettre plusieurs unités sur le même port d'émission. En conséquence, il y aurait eu des combinaisons interdites, comme émettre deux instructions entières + un branchement + un accès mémoire ; ou deux instructions entières + une opération flottante + un accès mémoire. De tells appariement interdits sont possibles en utilisant les six ports d'émission. ==L'émission multiple des accès mémoire== Les premiers processeurs superscalaires ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles. Par contre, c'est autre chose sur les processeurs superscalaires larges. Vu qu'ils travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares. L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. La ''load/store queue'' reste une structure unique, mais on peut lui rajouter des ports. Et à ce peit jeu, il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire. [[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]] 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 processeur utilisant cette technique est, par exemple, capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. Un exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. Ces processeurs pouvaient calculer trois adresses simultanément, mais ne pouvaient faire qu'un seul accès au cache par cycle. Le cout en circuit est modéré : les unités de calcul coutent très peu, les ports de la ''load/store queue'' coutent déjà un peu plus. Quelques processeurs utilisaient des solutions intermédiaires, un exemple classique étant les Pentium 1 et 2 d'Intel. Mais nous en parlerons dans le chapitre suivant. Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre. Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Le Pentium 4 était dans ce cas, mais il était un peu seul dans sa catégorie. Un cas similaire est celui où l'unité mémoire est alimentée par une fenêtre de micro-opération mélangeant instructions mémoire et entières. Les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. Dans ce cas, l'émission multiple est rarement implémentée. Le cas suivant a une file pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique. Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui pouvaient seulement émettre une lecture en même temps qu'une écriture. Ils avaient deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse. [[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]] Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture demande d'utiliser une file d'écriture multiport, ce qui est assez compliqué. Implémenter des lectures en plus est plus simple : il suffit de rajouter une seconde file de µops LOAD et une unité de calcul, alimentées via un second port d'émission. Il est aussi possible de rajouter un second port de lecture au cache, mais ce n'est pas obligatoire. Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse. De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports de lecture et d'écriture car il y a plus de lecture que d'écritures. Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. : Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse. [[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]] Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettent plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail. Pour résumer, l'évolution des CPU superscalaires a été la suivante : * pipeline mémorie fusionné avec le pipeline entier ; * séparation du pipeline mémoire du reste, avec émission parallèle des accès mémoire ; * émission multiple, mais sans dupliquer les ports du cache ; * émission multiple, avec émission simultanée d'une lecture et d’une écriture dans le cache ; * émission multiple avec plusieurs accès en lecture au cache. ==Les optimisations des processeurs superscalaires larges== Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul. ===L'étape de chargement d'un CPU superscalaire large=== Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 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 autoaligné.]] Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements ! [[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]] Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez 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 microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total). Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois 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 microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement. Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations. ===La micro-fusion des processeurs Intel et AMD=== La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''. Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''. Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout. La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Le parallélisme mémoire | prevText=Le parallélisme mémoire | next=Exemples de microarchitectures CPU : le cas du x86 | nextText=Exemples de microarchitectures CPU : le cas du x86 }} </noinclude> em36pbpgq8361jls6hy91y1w54j6yne 772704 772703 2026-09-21T17:25:39Z Mewtow 31375 /* L'émission multiple des accès mémoire */ 772704 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 tout premiers processeurs superscalaires pouvaient émettre deux µops en même temps, ce qui portait le nom de '''double émission'''. Mais le nombre a rapidement augmenté et les processeurs modernes 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 des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. Il y a donc des paires d'instructions autorisées et des paires interdites. En pratique, ces contraintes d’appariement ont peu d’impact. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière, le ratio est le même quand on remplace la multiplication par un branchement ou un accès mémoire. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Deux branchements consécutifs sont quant à eux plus fréquents, idem pour deux accès mémoire. Dupliquer des ALU entières est donc la priorité, suivi par la duplication des unités de branchement, puis celle des accès mémoire, pour terminer par le multiplieur et la FPU. ==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. Par exemple, l'unité de chargement charge deux fois plus d'instructions, mais n'est pas modifiée en profondeur. L'unité d'émission et les circuits d'exécution dans le désordre sont eux aussi altérés. Les unités de calcul sont souvent dupliquées, sauf sur les tout premiers CPU superscalaires. {|class="wikitable" |- ! rowspan="2" | Processeur sans émission multiple | rowspan="2" | Chargement | rowspan="2" | Décodage | rowspan="2" | Renommage | rowspan="2" | Émission | Exécution / ALU | rowspan="2" | ''Commit''/ROB |- | Exécution / ALU |- ! rowspan="4" | Processeur superscalaire | rowspan="4" | Chargement | rowspan="2" | Décodage | rowspan="4" | Renommage | rowspan="4" | Émission | Exécution / ALU | rowspan="4" | ''Commit''/ROB |- | Exécution / ALU |- | rowspan="2" | Décodage | Exécution / ALU |- | Exécution / ALU |} Voyons ce qu'il en est dans le détail. ===Les unités de calcul d'un processeur superscalaire=== Émettre N instructions simultanément demande implicitement d'exécuter N instructions par cycle, voire plus si des instructions multicycles viennent mettre leur grain de sel. 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 basique, avec seulement des ALU entières, peut se le permettre. Du moins, à condition de ne pas dupliquer l'unité mémoire, ce qui fait qu'il a pas mal de contraintes d'appariement. Par exemple, un processeur RISC basique avec seulement une ALU entière et un ''barrel shifter'' peut être rendu superscalaire en dupliquant les deux circuits. Le problème, est que cette solution marche assez mal dès que le processeur contient des circuits multiplieurs et/ou une FPU. Par exemple, un processeur à double émission devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, etc. 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 ! Par contre, l'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission. Un autre cas extrême est celui où aucune unité de calcul n'est dupliquée. Pour comprendre comment c'est possible rappelez-vous qu'un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et éventuellement une unité de branchement. Cela suffit pour créer un processeur quadruple émission, très limité. Un tel processeur peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire. Le problème est qu'il est rare que l'on ait à exécuter en même temps un calcul entier, un calcul flottant, un accès mémoire et un branchement. La quadruple émission n'est donc que rarement utilisée. En pratique, les seules combinaisons fréquentes sont les combinaisons calcul + accès mémoire et calcul + branchement. Les combinaisons calcul entier + calcul flottant sont aussi possibles, mais plus rares. Mais on dépasse rarement la double émission. En clair, à partir de la quadruple émission, on ne peut plus se contenter d'une ALU, d'une FPU, d'une unité de branchement et d'une unité mémoire. Les processeurs superscalaires larges n'ont pas le choix que de rajouter des unités de calcul. Reste à voir ce qui est dupliqué en pratique. 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. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant. ===Le chargement de plusieurs instructions consécutives=== Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large. Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence. Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas. Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement non-pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc. [[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]] ===Le décodage parallèle des instructions=== Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits. Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur. Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! ===L'unité d'émission d'un processeur superscalaire=== La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. 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 4 instructions, compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément deux 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. 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. 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 microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle. 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=== Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement. 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. ==L'évolution historique des processeurs superscalaires== Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs triple émission permettent eux d’émettre trois instructions à la fois. Les tout premiers processeurs superscalaires avaient un budget en transistors très limité, ce qui fait qu'ils ne pouvaient pas être "totalement superscalaires". Par exemple, un processeur double émission ne pouvait pas émettre deux opérations entières en même temps, ni deux opérations flottantes, ni une opération entière avec un accès mémoire. Il y avait des paires d'instructions autorisées et des paires interdites. Les '''contraintes d'appariement''' sur les instructions à émettre en même temps étaient très strictes. L'intérêt de ces contraintes d’appariement est que le processeur était très simple, il n'y avait pas besoin de rajouter des circuits complexes pour rendre le processeur superscalaire. Les CPU superscalaires historiques ne faisaient que mieux utiliser des unités de calcul déjà présentes. Prenons un processeur non-superscalaire simple, illustré ci-dessous. Il contient : une unité de chargement, un décodeur, une unité d'émission, une ALU, une FPU, les bancs de registre entier et flottants, et une unité mémoire. Nous avons placé l'unité mémoire à la suite de l'ALU entière, car ce sera important pour les explications qui vont suivre. [[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]] Les processeurs superscalaires historiques modifiaient assez peu le schéma précédent. L'unité de chargement était adaptée pour lire plusieurs instructions depuis la mémoire, il y avait un seul décodeur, l'unité d'émission était assez peu modifiée. Mais une chose était certaine : aucune unité de calcul n'était dupliquée, le banc de registre n'était presque pas modifié. A la rigueur, gérer la triple 'émission avec les branchements demandait parfois d'ajouter des ports de lecture sur le banc de registre généraux, pas plus. ===La double émission entière-flottante=== Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est d'émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Le processeur a donc deux pipelines séparés : un pour les micro-opérations entières, un autre pour les micro-opérations flottantes. On parle alors de '''double émission entière-flottante'''. {|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, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité. La plupart des premiers processeurs superscalaires étaient de ce type. Les exemples les plus notables sont les processeurs POWER 1 et ses dérivés comme le ''RISC Single Chip''. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle. [[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]] La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisations visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate]. ===L'émission parallèle des branchements=== Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur. 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 modifier le banc de registres. Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. [[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]] Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications et les divisions ? Pour étudier ces questions, nous allons prendre le cas d'un processeur capable d’émettre trois opérations en même temps : deux opérations entières, et une flottante, potentiellement d'autres. {|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 |} 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|Emission multiple des opérations entières, implémentation courante]] Pour les branchements, deux solutions sont possibles. Avec la première, il y a émission parallèle des branchements, à savoir qu'il est possible d'émettre en branchement en parallèle de deux-trois instructions entières. Et cela impose la présence d'une unité de branchement séparée, avec son port d'émission. Avec la seconde, les branchements sont traités comme des instructions entières. Soit les branchements sont exécutés dans une ALU entière, soit il y a une unité de branchement reliée à un port d'émission pour les opérations entières. [[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Emission série et parallèle des branchements]] Avec la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales, sur les processeurs superscalaires historiques. En clair, le processeur peut émettre 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. De plus, 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. Ce compromis change cependant quand on passe à la quadruple émission. {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 |- | * ALU entière * Multiplieur | * ALU entière * Barrel shifter * Unité de branchement * Unité mémoire | FPU |- | * Additions, soustractions, opérations bit à bit, etc. * Multiplication | * Additions, soustractions, opérations bit à bit, etc. * Décalages et rotations * Branchements * Accès mémoire |Opération flottante |} ===Les CPU superscalaires à plusieurs unités spécialisées=== Pour la triple émission, deux opérations entières + une flottante est l'idéal. Même les processeurs modernes à triple émission restent sur cette "norme". Mais arrivé à la quadruple émission, les CPU superscalaires ont évolués dans des directions différentes, chacun faisant à sa sauce. La raison est que la quadruple émission permet de nombreuses possibilités, mais la plupart ne sont pas intéressantes. Prenons un CPU superscalaire avec une ALU entière, une FPU, une unité de branchement, et une unité mémoire. Il est possible de créer un CPU quadruple émission avec ces unités, mais le gain en performance n'en vaudrait pas la peine. Il est rare qu'un bloc de 4 instructions contienne exactement une opération entière, une flottante, un branchement et un accès mémoire. En pratique, les seules combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. En conséquence, une fois arrivé à la quadruple émission, les processeurs superscalaires n'ont pas le choix que de rajouter des unités de calcul, voire des unités mémoire. Et ils ont privilégié l'ajout d'ALU entières. La raison est que les programmes informatiques utilisent beaucoup plus d'additions/soustractions que le reste. Il y a environ 5 opérations entières de base pour une multiplication entière, le ratio est le même quand on remplace la multiplication par un branchement ou un accès mémoire. En conséquence, les ALU entières ont été dupliquées en premier, et dans de nombreux exemplaires. Reste à voir comment a été dupliqué le reste. Les multiplieurs n'ont pas été dupliqués, sauf sur les processeurs superscalaires très larges. La raison est qu'il est rare de devoir exécuter deux multiplications à la fois. Et c'est la même chose pour le ''barrel shifter''. Deux branchements consécutifs sont quant à eux plus fréquents, c'est même assez courant. Aussi, les unités de branchement ont été dupliquées en second lieu, après les ALU entières. Avoir plusieurs accès mémoire proches est assez courant, surtout sur les CPU de type CISC. Aussi, les unités mémoire ont été dupliquées en troisième lieu. Pour résumer, dupliquer des ALU entières est donc la priorité, suivi par la duplication des unités de branchement, puis celle des accès mémoire, pour terminer par le multiplieur et le ''barrel shifter''. La FPU est un peu à part. Une autre tendance a été de séparer l'unité mémoire des ALU entières, ce qui a permis l''''émission parallèle des µops mémoire'''. Comme son nom l'indique, l'idée est d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière. La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple. La '''double émission flottante''' a été utilisée sur les processeurs Alpha. L'idée est d'émettre deux opérations flottantes à la fois. Sauf qu'une implémentation naïve demanderait de dupliquer la FPU, ce qui aurait un cout en circuit rédhibitoire. Pour rappel, les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant. L'idée est d'avoir des ports d'émission séparés pour l'additionneur flottant et le multiplieur flottant. Le processeur peut alors émettre une addition flottante en même temps qu'une multiplication flottante. Les autres circuits de calcul flottant sont répartis sur ces deux ports d'émission flottants. L'avantage est que cela se marie bien avec l'usage d'une fenêtre d'instruction séparée pour les opérations flottantes. La fenêtre d'instruction a alors deux ports séparés, au lieu d'un seul. Rajouter un second port d'émission flottant n'est pas trop un problème, car le cout lié à l'ajout d'un port n'est pas linéaire. Passer de un port à deux a un cout tolérable, bien plus que de passer de 3 ports à 4 ou de 4 à 5. Il est possible de dupliquer l'additionneur et le multiplieur flottant, ce qui permet d'émettre deux additions et multiplications flottantes en même temps. C'est ce qui est fait sur les processeurs AMD d'architecture Zen 1 et 2. Ils ont deux additionneurs flottants par cœur, deux multiplieurs flottants, chacun avec leur propre port d'émission. Les performances en calcul flottant sont assez impressionnantes pour un processeur de l'époque. [[File:ZEN - émission multiple flottante.png|centre|vignette|upright=2|Microarchitecture Zen 1 d'AMD.]] ==Les ports d'émission et de décodage== Les ports d'émission et de décodage sont des ressources matérielles comme les autres. Dans cette section, nous allons parler de concepts liés aux ports d'émission et de décodage. Nous allons notamment parler du partage des ports d'émission, ainsi que des relations numériques entre ces deux types de ports. ===Le partage des ports d'émission et la ''port contention''=== Pour le moment, nous n'avons vu que des cas où un port d'émission est relié soit à des ALU entières, soit à des FPU, pas aux deux. Mais il existe des processeurs où un même port d'émission alimente à la fois une ALU entière et une FPU. Par exemple, on peut relier un additionneur flottant sur le même port qu'une ALU entière. Il faut noter que cela implique une fenêtre d'instruction centralisée, capable de mettre en attente micro-opérations entières et flottantes. Un exemple est celui des processeurs Core 2 Duo. Ils disposent de 6 ports d'émission, dont 3 ports dédiés à l'unité mémoire. Les 3 ports restants alimentent chacun : une ALU entière, un circuit de calcul flottant et une unité de calcul SSE (une unité de calcul SIMD, qu'on abordera dans quelques chapitres). * Le premier port alimente une ALU entière simple et un multiplieur/diviseur flottant. * Le second alimente une ALU entière, un multiplieur entier et un additionneur flottant. * Le troisième alimente une ALU entière, sans circuit flottant dédié. Dans le même genre, il est possible de partager un port d'émission entre l'unité mémoire et une ALU entière. Cela permet d'utiliser l'ALU entière pour les calculs d'adresse, ce qui évite d'avoir à utiliser une unité de calcul d'adresse distincte. Un exemple est celui du processeur superscalaire double émission Power PC 440. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. L'organisation en question permet soit d'émettre un accès mémoire en même temps qu'une opération entière, soit d'émettre deux opérations entières simples, soit d’émettre une multiplication et une addition/soustraction/comparaison. Une organisation simple, mais très efficace ! [[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]] ===La séparation entre largeur de décodage et d'émission=== Sur les processeurs superscalaires larges, le processeur peut émettre plus d'instructions qu'il peut en décoder. Plus haut, j'ai dit que l'unité d'émission avait parfois plus de ports d'émission que de ports de décodage. La raison est assez simple, et elle s'explique assez bien avec un exemple. Prenons un processeur avec trois ALU entières, une FPU, une unité de branchement et une unité mémoire. Chacune a son propre port d'émission, ce qui fait que le CPU est capable d'émettre 6 µops. Du moins, c'est la théorie, car les conditions pour sont assez rares. Il faut qu'en chargeant un paquet de 6 instructions, il y ait exactement trois instructions entières, une flottante, un branchement et un accès mémoire. Si le paquet contient 5 instructions entières et un branchement, seules quatre instructions sont émises. Dans cet exemple, dans la grande majorité des cas, seuls 4 ports seront utilisés en moyenne et deux seront sous-utilisés. Il s'agit là d'une moyenne, qui dépend du code, et des instructions exactes à exécuter. Cependant, cette moyenne nous donne un moyen d'économiser beaucoup de circuits pour un cout en performance mineur. L'idée est de ne charger que des blocs de 4 instructions, mais d'avoir 6 ports d'émission. Le processeur charge et décode 4 instructions, les quatre instructions sont envoyées sur les ports d'émission adéquat. Le cout en transistors est alors situé entre celui d'un CPU quadruple émission et sextuple émission. Et les performances sont aussi entre les deux. Pour les performances, c'est parce que cela permet d'avoir un CPU quadruple émission avec bien moins de contraintes d’appariement. Imaginez qu'on se soit limité à quatre ports d'émission : il aurait fallu mettre plusieurs unités sur le même port d'émission. En conséquence, il y aurait eu des combinaisons interdites, comme émettre deux instructions entières + un branchement + un accès mémoire ; ou deux instructions entières + une opération flottante + un accès mémoire. De tells appariement interdits sont possibles en utilisant les six ports d'émission. ==L'émission multiple des accès mémoire== Les premiers processeurs superscalaires ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles. Par contre, c'est autre chose sur les processeurs superscalaires larges. Vu qu'ils travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares. L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. La ''load/store queue'' reste une structure unique, mais on peut lui rajouter des ports. Et à ce peit jeu, il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire. [[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]] 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. Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre. Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Le Pentium 4 était dans ce cas, mais il était un peu seul dans sa catégorie. Un cas similaire est celui où l'unité mémoire est alimentée par une fenêtre de micro-opération mélangeant instructions mémoire et entières. Les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. Dans ce cas, l'émission multiple est rarement implémentée. Le cas suivant a une file pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique. Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui pouvaient seulement émettre une lecture en même temps qu'une écriture. Ils avaient deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse. [[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]] Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture demande d'utiliser une file d'écriture multiport, ce qui est assez compliqué. Implémenter des lectures en plus est plus simple : il suffit de rajouter une seconde file de µops LOAD et une unité de calcul, alimentées via un second port d'émission. Il est aussi possible de rajouter un second port de lecture au cache, mais ce n'est pas obligatoire. Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse. De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports de lecture et d'écriture car il y a plus de lecture que d'écritures. Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. : Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse. [[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]] Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettent plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail. Pour résumer, l'évolution des CPU superscalaires a été la suivante : * pipeline mémorie fusionné avec le pipeline entier ; * séparation du pipeline mémoire du reste, avec émission parallèle des accès mémoire ; * émission multiple, mais sans dupliquer les ports du cache ; * émission multiple, avec émission simultanée d'une lecture et d’une écriture dans le cache ; * émission multiple avec plusieurs accès en lecture au cache. ==Les optimisations des processeurs superscalaires larges== Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul. ===L'étape de chargement d'un CPU superscalaire large=== Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 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 autoaligné.]] Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements ! [[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]] Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez 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 microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total). Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois 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 microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement. Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations. ===La micro-fusion des processeurs Intel et AMD=== La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''. Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''. Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout. La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Le parallélisme mémoire | prevText=Le parallélisme mémoire | next=Exemples de microarchitectures CPU : le cas du x86 | nextText=Exemples de microarchitectures CPU : le cas du x86 }} </noinclude> a11b0y7zdl0ba5vjk0uutywycllh1ef 772710 772704 2026-09-21T18:10:12Z Mewtow 31375 /* Les contraintes d’appariement */ 772710 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 tout premiers processeurs superscalaires pouvaient émettre deux µops en même temps, ce qui portait le nom de '''double émission'''. Mais le nombre a rapidement augmenté et les processeurs modernes 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 des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. Il y a donc des paires d'instructions autorisées et des paires interdites. En pratique, ces contraintes d’appariement ont peu d’impact. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Deux branchements consécutifs sont quant à eux plus fréquents, mais exécuter deux branchements en même temps pose un problème : que faire quand les deux sont pris ? Deux accès mémoire consécutifs ou proches est assez fréquent, et dupliquer l'unité mémoire est compliqué, bien que possible. Dupliquer des ALU entières est donc la priorité, suivi par la duplication des unités mémoire, pour terminer par le multiplieur et la FPU. ==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. Par exemple, l'unité de chargement charge deux fois plus d'instructions, mais n'est pas modifiée en profondeur. L'unité d'émission et les circuits d'exécution dans le désordre sont eux aussi altérés. Les unités de calcul sont souvent dupliquées, sauf sur les tout premiers CPU superscalaires. {|class="wikitable" |- ! rowspan="2" | Processeur sans émission multiple | rowspan="2" | Chargement | rowspan="2" | Décodage | rowspan="2" | Renommage | rowspan="2" | Émission | Exécution / ALU | rowspan="2" | ''Commit''/ROB |- | Exécution / ALU |- ! rowspan="4" | Processeur superscalaire | rowspan="4" | Chargement | rowspan="2" | Décodage | rowspan="4" | Renommage | rowspan="4" | Émission | Exécution / ALU | rowspan="4" | ''Commit''/ROB |- | Exécution / ALU |- | rowspan="2" | Décodage | Exécution / ALU |- | Exécution / ALU |} Voyons ce qu'il en est dans le détail. ===Les unités de calcul d'un processeur superscalaire=== Émettre N instructions simultanément demande implicitement d'exécuter N instructions par cycle, voire plus si des instructions multicycles viennent mettre leur grain de sel. 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 basique, avec seulement des ALU entières, peut se le permettre. Du moins, à condition de ne pas dupliquer l'unité mémoire, ce qui fait qu'il a pas mal de contraintes d'appariement. Par exemple, un processeur RISC basique avec seulement une ALU entière et un ''barrel shifter'' peut être rendu superscalaire en dupliquant les deux circuits. Le problème, est que cette solution marche assez mal dès que le processeur contient des circuits multiplieurs et/ou une FPU. Par exemple, un processeur à double émission devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, etc. 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 ! Par contre, l'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission. Un autre cas extrême est celui où aucune unité de calcul n'est dupliquée. Pour comprendre comment c'est possible rappelez-vous qu'un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et éventuellement une unité de branchement. Cela suffit pour créer un processeur quadruple émission, très limité. Un tel processeur peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire. Le problème est qu'il est rare que l'on ait à exécuter en même temps un calcul entier, un calcul flottant, un accès mémoire et un branchement. La quadruple émission n'est donc que rarement utilisée. En pratique, les seules combinaisons fréquentes sont les combinaisons calcul + accès mémoire et calcul + branchement. Les combinaisons calcul entier + calcul flottant sont aussi possibles, mais plus rares. Mais on dépasse rarement la double émission. En clair, à partir de la quadruple émission, on ne peut plus se contenter d'une ALU, d'une FPU, d'une unité de branchement et d'une unité mémoire. Les processeurs superscalaires larges n'ont pas le choix que de rajouter des unités de calcul. Reste à voir ce qui est dupliqué en pratique. 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. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant. ===Le chargement de plusieurs instructions consécutives=== Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large. Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence. Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas. Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement non-pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc. [[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]] ===Le décodage parallèle des instructions=== Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits. Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur. Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! ===L'unité d'émission d'un processeur superscalaire=== La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. 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 4 instructions, compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément deux 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. 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. 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 microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle. 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=== Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement. 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. ==L'évolution historique des processeurs superscalaires== Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs triple émission permettent eux d’émettre trois instructions à la fois. Les tout premiers processeurs superscalaires avaient un budget en transistors très limité, ce qui fait qu'ils ne pouvaient pas être "totalement superscalaires". Par exemple, un processeur double émission ne pouvait pas émettre deux opérations entières en même temps, ni deux opérations flottantes, ni une opération entière avec un accès mémoire. Il y avait des paires d'instructions autorisées et des paires interdites. Les '''contraintes d'appariement''' sur les instructions à émettre en même temps étaient très strictes. L'intérêt de ces contraintes d’appariement est que le processeur était très simple, il n'y avait pas besoin de rajouter des circuits complexes pour rendre le processeur superscalaire. Les CPU superscalaires historiques ne faisaient que mieux utiliser des unités de calcul déjà présentes. Prenons un processeur non-superscalaire simple, illustré ci-dessous. Il contient : une unité de chargement, un décodeur, une unité d'émission, une ALU, une FPU, les bancs de registre entier et flottants, et une unité mémoire. Nous avons placé l'unité mémoire à la suite de l'ALU entière, car ce sera important pour les explications qui vont suivre. [[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]] Les processeurs superscalaires historiques modifiaient assez peu le schéma précédent. L'unité de chargement était adaptée pour lire plusieurs instructions depuis la mémoire, il y avait un seul décodeur, l'unité d'émission était assez peu modifiée. Mais une chose était certaine : aucune unité de calcul n'était dupliquée, le banc de registre n'était presque pas modifié. A la rigueur, gérer la triple 'émission avec les branchements demandait parfois d'ajouter des ports de lecture sur le banc de registre généraux, pas plus. ===La double émission entière-flottante=== Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est d'émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Le processeur a donc deux pipelines séparés : un pour les micro-opérations entières, un autre pour les micro-opérations flottantes. On parle alors de '''double émission entière-flottante'''. {|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, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité. La plupart des premiers processeurs superscalaires étaient de ce type. Les exemples les plus notables sont les processeurs POWER 1 et ses dérivés comme le ''RISC Single Chip''. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle. [[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]] La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisations visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate]. ===L'émission parallèle des branchements=== Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur. 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 modifier le banc de registres. Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. [[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]] Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications et les divisions ? Pour étudier ces questions, nous allons prendre le cas d'un processeur capable d’émettre trois opérations en même temps : deux opérations entières, et une flottante, potentiellement d'autres. {|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 |} 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|Emission multiple des opérations entières, implémentation courante]] Pour les branchements, deux solutions sont possibles. Avec la première, il y a émission parallèle des branchements, à savoir qu'il est possible d'émettre en branchement en parallèle de deux-trois instructions entières. Et cela impose la présence d'une unité de branchement séparée, avec son port d'émission. Avec la seconde, les branchements sont traités comme des instructions entières. Soit les branchements sont exécutés dans une ALU entière, soit il y a une unité de branchement reliée à un port d'émission pour les opérations entières. [[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Emission série et parallèle des branchements]] Avec la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales, sur les processeurs superscalaires historiques. En clair, le processeur peut émettre 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. De plus, 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. Ce compromis change cependant quand on passe à la quadruple émission. {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 |- | * ALU entière * Multiplieur | * ALU entière * Barrel shifter * Unité de branchement * Unité mémoire | FPU |- | * Additions, soustractions, opérations bit à bit, etc. * Multiplication | * Additions, soustractions, opérations bit à bit, etc. * Décalages et rotations * Branchements * Accès mémoire |Opération flottante |} ===Les CPU superscalaires à plusieurs unités spécialisées=== Pour la triple émission, deux opérations entières + une flottante est l'idéal. Même les processeurs modernes à triple émission restent sur cette "norme". Mais arrivé à la quadruple émission, les CPU superscalaires ont évolués dans des directions différentes, chacun faisant à sa sauce. La raison est que la quadruple émission permet de nombreuses possibilités, mais la plupart ne sont pas intéressantes. Prenons un CPU superscalaire avec une ALU entière, une FPU, une unité de branchement, et une unité mémoire. Il est possible de créer un CPU quadruple émission avec ces unités, mais le gain en performance n'en vaudrait pas la peine. Il est rare qu'un bloc de 4 instructions contienne exactement une opération entière, une flottante, un branchement et un accès mémoire. En pratique, les seules combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. En conséquence, une fois arrivé à la quadruple émission, les processeurs superscalaires n'ont pas le choix que de rajouter des unités de calcul, voire des unités mémoire. Et ils ont privilégié l'ajout d'ALU entières. La raison est que les programmes informatiques utilisent beaucoup plus d'additions/soustractions que le reste. Il y a environ 5 opérations entières de base pour une multiplication entière, le ratio est le même quand on remplace la multiplication par un branchement ou un accès mémoire. En conséquence, les ALU entières ont été dupliquées en premier, et dans de nombreux exemplaires. Reste à voir comment a été dupliqué le reste. Les multiplieurs n'ont pas été dupliqués, sauf sur les processeurs superscalaires très larges. La raison est qu'il est rare de devoir exécuter deux multiplications à la fois. Et c'est la même chose pour le ''barrel shifter''. Deux branchements consécutifs sont quant à eux plus fréquents, c'est même assez courant. Aussi, les unités de branchement ont été dupliquées en second lieu, après les ALU entières. Avoir plusieurs accès mémoire proches est assez courant, surtout sur les CPU de type CISC. Aussi, les unités mémoire ont été dupliquées en troisième lieu. Pour résumer, dupliquer des ALU entières est donc la priorité, suivi par la duplication des unités de branchement, puis celle des accès mémoire, pour terminer par le multiplieur et le ''barrel shifter''. La FPU est un peu à part. Une autre tendance a été de séparer l'unité mémoire des ALU entières, ce qui a permis l''''émission parallèle des µops mémoire'''. Comme son nom l'indique, l'idée est d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière. La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple. La '''double émission flottante''' a été utilisée sur les processeurs Alpha. L'idée est d'émettre deux opérations flottantes à la fois. Sauf qu'une implémentation naïve demanderait de dupliquer la FPU, ce qui aurait un cout en circuit rédhibitoire. Pour rappel, les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant. L'idée est d'avoir des ports d'émission séparés pour l'additionneur flottant et le multiplieur flottant. Le processeur peut alors émettre une addition flottante en même temps qu'une multiplication flottante. Les autres circuits de calcul flottant sont répartis sur ces deux ports d'émission flottants. L'avantage est que cela se marie bien avec l'usage d'une fenêtre d'instruction séparée pour les opérations flottantes. La fenêtre d'instruction a alors deux ports séparés, au lieu d'un seul. Rajouter un second port d'émission flottant n'est pas trop un problème, car le cout lié à l'ajout d'un port n'est pas linéaire. Passer de un port à deux a un cout tolérable, bien plus que de passer de 3 ports à 4 ou de 4 à 5. Il est possible de dupliquer l'additionneur et le multiplieur flottant, ce qui permet d'émettre deux additions et multiplications flottantes en même temps. C'est ce qui est fait sur les processeurs AMD d'architecture Zen 1 et 2. Ils ont deux additionneurs flottants par cœur, deux multiplieurs flottants, chacun avec leur propre port d'émission. Les performances en calcul flottant sont assez impressionnantes pour un processeur de l'époque. [[File:ZEN - émission multiple flottante.png|centre|vignette|upright=2|Microarchitecture Zen 1 d'AMD.]] ==Les ports d'émission et de décodage== Les ports d'émission et de décodage sont des ressources matérielles comme les autres. Dans cette section, nous allons parler de concepts liés aux ports d'émission et de décodage. Nous allons notamment parler du partage des ports d'émission, ainsi que des relations numériques entre ces deux types de ports. ===Le partage des ports d'émission et la ''port contention''=== Pour le moment, nous n'avons vu que des cas où un port d'émission est relié soit à des ALU entières, soit à des FPU, pas aux deux. Mais il existe des processeurs où un même port d'émission alimente à la fois une ALU entière et une FPU. Par exemple, on peut relier un additionneur flottant sur le même port qu'une ALU entière. Il faut noter que cela implique une fenêtre d'instruction centralisée, capable de mettre en attente micro-opérations entières et flottantes. Un exemple est celui des processeurs Core 2 Duo. Ils disposent de 6 ports d'émission, dont 3 ports dédiés à l'unité mémoire. Les 3 ports restants alimentent chacun : une ALU entière, un circuit de calcul flottant et une unité de calcul SSE (une unité de calcul SIMD, qu'on abordera dans quelques chapitres). * Le premier port alimente une ALU entière simple et un multiplieur/diviseur flottant. * Le second alimente une ALU entière, un multiplieur entier et un additionneur flottant. * Le troisième alimente une ALU entière, sans circuit flottant dédié. Dans le même genre, il est possible de partager un port d'émission entre l'unité mémoire et une ALU entière. Cela permet d'utiliser l'ALU entière pour les calculs d'adresse, ce qui évite d'avoir à utiliser une unité de calcul d'adresse distincte. Un exemple est celui du processeur superscalaire double émission Power PC 440. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. L'organisation en question permet soit d'émettre un accès mémoire en même temps qu'une opération entière, soit d'émettre deux opérations entières simples, soit d’émettre une multiplication et une addition/soustraction/comparaison. Une organisation simple, mais très efficace ! [[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]] ===La séparation entre largeur de décodage et d'émission=== Sur les processeurs superscalaires larges, le processeur peut émettre plus d'instructions qu'il peut en décoder. Plus haut, j'ai dit que l'unité d'émission avait parfois plus de ports d'émission que de ports de décodage. La raison est assez simple, et elle s'explique assez bien avec un exemple. Prenons un processeur avec trois ALU entières, une FPU, une unité de branchement et une unité mémoire. Chacune a son propre port d'émission, ce qui fait que le CPU est capable d'émettre 6 µops. Du moins, c'est la théorie, car les conditions pour sont assez rares. Il faut qu'en chargeant un paquet de 6 instructions, il y ait exactement trois instructions entières, une flottante, un branchement et un accès mémoire. Si le paquet contient 5 instructions entières et un branchement, seules quatre instructions sont émises. Dans cet exemple, dans la grande majorité des cas, seuls 4 ports seront utilisés en moyenne et deux seront sous-utilisés. Il s'agit là d'une moyenne, qui dépend du code, et des instructions exactes à exécuter. Cependant, cette moyenne nous donne un moyen d'économiser beaucoup de circuits pour un cout en performance mineur. L'idée est de ne charger que des blocs de 4 instructions, mais d'avoir 6 ports d'émission. Le processeur charge et décode 4 instructions, les quatre instructions sont envoyées sur les ports d'émission adéquat. Le cout en transistors est alors situé entre celui d'un CPU quadruple émission et sextuple émission. Et les performances sont aussi entre les deux. Pour les performances, c'est parce que cela permet d'avoir un CPU quadruple émission avec bien moins de contraintes d’appariement. Imaginez qu'on se soit limité à quatre ports d'émission : il aurait fallu mettre plusieurs unités sur le même port d'émission. En conséquence, il y aurait eu des combinaisons interdites, comme émettre deux instructions entières + un branchement + un accès mémoire ; ou deux instructions entières + une opération flottante + un accès mémoire. De tells appariement interdits sont possibles en utilisant les six ports d'émission. ==L'émission multiple des accès mémoire== Les premiers processeurs superscalaires ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles. Par contre, c'est autre chose sur les processeurs superscalaires larges. Vu qu'ils travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares. L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. La ''load/store queue'' reste une structure unique, mais on peut lui rajouter des ports. Et à ce peit jeu, il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire. [[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]] 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. Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre. Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Le Pentium 4 était dans ce cas, mais il était un peu seul dans sa catégorie. Un cas similaire est celui où l'unité mémoire est alimentée par une fenêtre de micro-opération mélangeant instructions mémoire et entières. Les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. Dans ce cas, l'émission multiple est rarement implémentée. Le cas suivant a une file pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique. Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui pouvaient seulement émettre une lecture en même temps qu'une écriture. Ils avaient deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse. [[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]] Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture demande d'utiliser une file d'écriture multiport, ce qui est assez compliqué. Implémenter des lectures en plus est plus simple : il suffit de rajouter une seconde file de µops LOAD et une unité de calcul, alimentées via un second port d'émission. Il est aussi possible de rajouter un second port de lecture au cache, mais ce n'est pas obligatoire. Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse. De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports de lecture et d'écriture car il y a plus de lecture que d'écritures. Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. : Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse. [[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]] Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettent plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail. Pour résumer, l'évolution des CPU superscalaires a été la suivante : * pipeline mémorie fusionné avec le pipeline entier ; * séparation du pipeline mémoire du reste, avec émission parallèle des accès mémoire ; * émission multiple, mais sans dupliquer les ports du cache ; * émission multiple, avec émission simultanée d'une lecture et d’une écriture dans le cache ; * émission multiple avec plusieurs accès en lecture au cache. ==Les optimisations des processeurs superscalaires larges== Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul. ===L'étape de chargement d'un CPU superscalaire large=== Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 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 autoaligné.]] Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements ! [[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]] Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez 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 microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total). Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois 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 microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement. Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations. ===La micro-fusion des processeurs Intel et AMD=== La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''. Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''. Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout. La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Le parallélisme mémoire | prevText=Le parallélisme mémoire | next=Exemples de microarchitectures CPU : le cas du x86 | nextText=Exemples de microarchitectures CPU : le cas du x86 }} </noinclude> pun1c7nelm3qj9vy2lxj3l3lkb6bz7v 772711 772710 2026-09-21T18:11:34Z Mewtow 31375 /* Les unités de calcul d'un processeur superscalaire */ 772711 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 tout premiers processeurs superscalaires pouvaient émettre deux µops en même temps, ce qui portait le nom de '''double émission'''. Mais le nombre a rapidement augmenté et les processeurs modernes 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 des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. Il y a donc des paires d'instructions autorisées et des paires interdites. En pratique, ces contraintes d’appariement ont peu d’impact. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Deux branchements consécutifs sont quant à eux plus fréquents, mais exécuter deux branchements en même temps pose un problème : que faire quand les deux sont pris ? Deux accès mémoire consécutifs ou proches est assez fréquent, et dupliquer l'unité mémoire est compliqué, bien que possible. Dupliquer des ALU entières est donc la priorité, suivi par la duplication des unités mémoire, pour terminer par le multiplieur et la FPU. ==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. Par exemple, l'unité de chargement charge deux fois plus d'instructions, mais n'est pas modifiée en profondeur. L'unité d'émission et les circuits d'exécution dans le désordre sont eux aussi altérés. Les unités de calcul sont souvent dupliquées, sauf sur les tout premiers CPU superscalaires. {|class="wikitable" |- ! rowspan="2" | Processeur sans émission multiple | rowspan="2" | Chargement | rowspan="2" | Décodage | rowspan="2" | Renommage | rowspan="2" | Émission | Exécution / ALU | rowspan="2" | ''Commit''/ROB |- | Exécution / ALU |- ! rowspan="4" | Processeur superscalaire | rowspan="4" | Chargement | rowspan="2" | Décodage | rowspan="4" | Renommage | rowspan="4" | Émission | Exécution / ALU | rowspan="4" | ''Commit''/ROB |- | Exécution / ALU |- | rowspan="2" | Décodage | Exécution / ALU |- | Exécution / ALU |} Voyons ce qu'il en est dans le détail. ===Les unités de calcul d'un processeur superscalaire=== Émettre N instructions simultanément demande implicitement d'exécuter N instructions par cycle, voire plus si des instructions multicycles viennent mettre leur grain de sel. 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 basique, avec seulement des ALU entières, peut se le permettre. Du moins, à condition de ne pas dupliquer l'unité mémoire, ce qui fait qu'il a pas mal de contraintes d'appariement. Par exemple, un processeur RISC basique avec seulement une ALU entière et un ''barrel shifter'' peut être rendu superscalaire en dupliquant les deux circuits. Le problème, est que cette solution marche assez mal dès que le processeur contient des circuits multiplieurs et/ou une FPU. Par exemple, un processeur à double émission devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, etc. 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 ! Par contre, l'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission. Un autre cas extrême est celui où aucune unité de calcul n'est dupliquée. Pour comprendre comment c'est possible rappelez-vous qu'un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et éventuellement une unité de branchement. Cela suffit pour créer un processeur quadruple émission, très limité. Un tel processeur peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire. Le problème est qu'il est rare que l'on ait à exécuter en même temps un calcul entier, un calcul flottant, un accès mémoire et un branchement. En pratique, les seules combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser la quadruple émission n'est donc pas une bonne idée. 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. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant. ===Le chargement de plusieurs instructions consécutives=== Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large. Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence. Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas. Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement non-pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc. [[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]] ===Le décodage parallèle des instructions=== Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits. Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur. Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! ===L'unité d'émission d'un processeur superscalaire=== La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. 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 4 instructions, compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément deux 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. 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. 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 microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle. 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=== Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement. 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. ==L'évolution historique des processeurs superscalaires== Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs triple émission permettent eux d’émettre trois instructions à la fois. Les tout premiers processeurs superscalaires avaient un budget en transistors très limité, ce qui fait qu'ils ne pouvaient pas être "totalement superscalaires". Par exemple, un processeur double émission ne pouvait pas émettre deux opérations entières en même temps, ni deux opérations flottantes, ni une opération entière avec un accès mémoire. Il y avait des paires d'instructions autorisées et des paires interdites. Les '''contraintes d'appariement''' sur les instructions à émettre en même temps étaient très strictes. L'intérêt de ces contraintes d’appariement est que le processeur était très simple, il n'y avait pas besoin de rajouter des circuits complexes pour rendre le processeur superscalaire. Les CPU superscalaires historiques ne faisaient que mieux utiliser des unités de calcul déjà présentes. Prenons un processeur non-superscalaire simple, illustré ci-dessous. Il contient : une unité de chargement, un décodeur, une unité d'émission, une ALU, une FPU, les bancs de registre entier et flottants, et une unité mémoire. Nous avons placé l'unité mémoire à la suite de l'ALU entière, car ce sera important pour les explications qui vont suivre. [[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]] Les processeurs superscalaires historiques modifiaient assez peu le schéma précédent. L'unité de chargement était adaptée pour lire plusieurs instructions depuis la mémoire, il y avait un seul décodeur, l'unité d'émission était assez peu modifiée. Mais une chose était certaine : aucune unité de calcul n'était dupliquée, le banc de registre n'était presque pas modifié. A la rigueur, gérer la triple 'émission avec les branchements demandait parfois d'ajouter des ports de lecture sur le banc de registre généraux, pas plus. ===La double émission entière-flottante=== Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est d'émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Le processeur a donc deux pipelines séparés : un pour les micro-opérations entières, un autre pour les micro-opérations flottantes. On parle alors de '''double émission entière-flottante'''. {|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, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité. La plupart des premiers processeurs superscalaires étaient de ce type. Les exemples les plus notables sont les processeurs POWER 1 et ses dérivés comme le ''RISC Single Chip''. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle. [[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]] La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisations visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate]. ===L'émission parallèle des branchements=== Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur. 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 modifier le banc de registres. Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. [[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]] Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications et les divisions ? Pour étudier ces questions, nous allons prendre le cas d'un processeur capable d’émettre trois opérations en même temps : deux opérations entières, et une flottante, potentiellement d'autres. {|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 |} 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|Emission multiple des opérations entières, implémentation courante]] Pour les branchements, deux solutions sont possibles. Avec la première, il y a émission parallèle des branchements, à savoir qu'il est possible d'émettre en branchement en parallèle de deux-trois instructions entières. Et cela impose la présence d'une unité de branchement séparée, avec son port d'émission. Avec la seconde, les branchements sont traités comme des instructions entières. Soit les branchements sont exécutés dans une ALU entière, soit il y a une unité de branchement reliée à un port d'émission pour les opérations entières. [[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Emission série et parallèle des branchements]] Avec la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales, sur les processeurs superscalaires historiques. En clair, le processeur peut émettre 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. De plus, 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. Ce compromis change cependant quand on passe à la quadruple émission. {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 |- | * ALU entière * Multiplieur | * ALU entière * Barrel shifter * Unité de branchement * Unité mémoire | FPU |- | * Additions, soustractions, opérations bit à bit, etc. * Multiplication | * Additions, soustractions, opérations bit à bit, etc. * Décalages et rotations * Branchements * Accès mémoire |Opération flottante |} ===Les CPU superscalaires à plusieurs unités spécialisées=== Pour la triple émission, deux opérations entières + une flottante est l'idéal. Même les processeurs modernes à triple émission restent sur cette "norme". Mais arrivé à la quadruple émission, les CPU superscalaires ont évolués dans des directions différentes, chacun faisant à sa sauce. La raison est que la quadruple émission permet de nombreuses possibilités, mais la plupart ne sont pas intéressantes. Prenons un CPU superscalaire avec une ALU entière, une FPU, une unité de branchement, et une unité mémoire. Il est possible de créer un CPU quadruple émission avec ces unités, mais le gain en performance n'en vaudrait pas la peine. Il est rare qu'un bloc de 4 instructions contienne exactement une opération entière, une flottante, un branchement et un accès mémoire. En pratique, les seules combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. En conséquence, une fois arrivé à la quadruple émission, les processeurs superscalaires n'ont pas le choix que de rajouter des unités de calcul, voire des unités mémoire. Et ils ont privilégié l'ajout d'ALU entières. La raison est que les programmes informatiques utilisent beaucoup plus d'additions/soustractions que le reste. Il y a environ 5 opérations entières de base pour une multiplication entière, le ratio est le même quand on remplace la multiplication par un branchement ou un accès mémoire. En conséquence, les ALU entières ont été dupliquées en premier, et dans de nombreux exemplaires. Reste à voir comment a été dupliqué le reste. Les multiplieurs n'ont pas été dupliqués, sauf sur les processeurs superscalaires très larges. La raison est qu'il est rare de devoir exécuter deux multiplications à la fois. Et c'est la même chose pour le ''barrel shifter''. Deux branchements consécutifs sont quant à eux plus fréquents, c'est même assez courant. Aussi, les unités de branchement ont été dupliquées en second lieu, après les ALU entières. Avoir plusieurs accès mémoire proches est assez courant, surtout sur les CPU de type CISC. Aussi, les unités mémoire ont été dupliquées en troisième lieu. Pour résumer, dupliquer des ALU entières est donc la priorité, suivi par la duplication des unités de branchement, puis celle des accès mémoire, pour terminer par le multiplieur et le ''barrel shifter''. La FPU est un peu à part. Une autre tendance a été de séparer l'unité mémoire des ALU entières, ce qui a permis l''''émission parallèle des µops mémoire'''. Comme son nom l'indique, l'idée est d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière. La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple. La '''double émission flottante''' a été utilisée sur les processeurs Alpha. L'idée est d'émettre deux opérations flottantes à la fois. Sauf qu'une implémentation naïve demanderait de dupliquer la FPU, ce qui aurait un cout en circuit rédhibitoire. Pour rappel, les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant. L'idée est d'avoir des ports d'émission séparés pour l'additionneur flottant et le multiplieur flottant. Le processeur peut alors émettre une addition flottante en même temps qu'une multiplication flottante. Les autres circuits de calcul flottant sont répartis sur ces deux ports d'émission flottants. L'avantage est que cela se marie bien avec l'usage d'une fenêtre d'instruction séparée pour les opérations flottantes. La fenêtre d'instruction a alors deux ports séparés, au lieu d'un seul. Rajouter un second port d'émission flottant n'est pas trop un problème, car le cout lié à l'ajout d'un port n'est pas linéaire. Passer de un port à deux a un cout tolérable, bien plus que de passer de 3 ports à 4 ou de 4 à 5. Il est possible de dupliquer l'additionneur et le multiplieur flottant, ce qui permet d'émettre deux additions et multiplications flottantes en même temps. C'est ce qui est fait sur les processeurs AMD d'architecture Zen 1 et 2. Ils ont deux additionneurs flottants par cœur, deux multiplieurs flottants, chacun avec leur propre port d'émission. Les performances en calcul flottant sont assez impressionnantes pour un processeur de l'époque. [[File:ZEN - émission multiple flottante.png|centre|vignette|upright=2|Microarchitecture Zen 1 d'AMD.]] ==Les ports d'émission et de décodage== Les ports d'émission et de décodage sont des ressources matérielles comme les autres. Dans cette section, nous allons parler de concepts liés aux ports d'émission et de décodage. Nous allons notamment parler du partage des ports d'émission, ainsi que des relations numériques entre ces deux types de ports. ===Le partage des ports d'émission et la ''port contention''=== Pour le moment, nous n'avons vu que des cas où un port d'émission est relié soit à des ALU entières, soit à des FPU, pas aux deux. Mais il existe des processeurs où un même port d'émission alimente à la fois une ALU entière et une FPU. Par exemple, on peut relier un additionneur flottant sur le même port qu'une ALU entière. Il faut noter que cela implique une fenêtre d'instruction centralisée, capable de mettre en attente micro-opérations entières et flottantes. Un exemple est celui des processeurs Core 2 Duo. Ils disposent de 6 ports d'émission, dont 3 ports dédiés à l'unité mémoire. Les 3 ports restants alimentent chacun : une ALU entière, un circuit de calcul flottant et une unité de calcul SSE (une unité de calcul SIMD, qu'on abordera dans quelques chapitres). * Le premier port alimente une ALU entière simple et un multiplieur/diviseur flottant. * Le second alimente une ALU entière, un multiplieur entier et un additionneur flottant. * Le troisième alimente une ALU entière, sans circuit flottant dédié. Dans le même genre, il est possible de partager un port d'émission entre l'unité mémoire et une ALU entière. Cela permet d'utiliser l'ALU entière pour les calculs d'adresse, ce qui évite d'avoir à utiliser une unité de calcul d'adresse distincte. Un exemple est celui du processeur superscalaire double émission Power PC 440. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. L'organisation en question permet soit d'émettre un accès mémoire en même temps qu'une opération entière, soit d'émettre deux opérations entières simples, soit d’émettre une multiplication et une addition/soustraction/comparaison. Une organisation simple, mais très efficace ! [[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]] ===La séparation entre largeur de décodage et d'émission=== Sur les processeurs superscalaires larges, le processeur peut émettre plus d'instructions qu'il peut en décoder. Plus haut, j'ai dit que l'unité d'émission avait parfois plus de ports d'émission que de ports de décodage. La raison est assez simple, et elle s'explique assez bien avec un exemple. Prenons un processeur avec trois ALU entières, une FPU, une unité de branchement et une unité mémoire. Chacune a son propre port d'émission, ce qui fait que le CPU est capable d'émettre 6 µops. Du moins, c'est la théorie, car les conditions pour sont assez rares. Il faut qu'en chargeant un paquet de 6 instructions, il y ait exactement trois instructions entières, une flottante, un branchement et un accès mémoire. Si le paquet contient 5 instructions entières et un branchement, seules quatre instructions sont émises. Dans cet exemple, dans la grande majorité des cas, seuls 4 ports seront utilisés en moyenne et deux seront sous-utilisés. Il s'agit là d'une moyenne, qui dépend du code, et des instructions exactes à exécuter. Cependant, cette moyenne nous donne un moyen d'économiser beaucoup de circuits pour un cout en performance mineur. L'idée est de ne charger que des blocs de 4 instructions, mais d'avoir 6 ports d'émission. Le processeur charge et décode 4 instructions, les quatre instructions sont envoyées sur les ports d'émission adéquat. Le cout en transistors est alors situé entre celui d'un CPU quadruple émission et sextuple émission. Et les performances sont aussi entre les deux. Pour les performances, c'est parce que cela permet d'avoir un CPU quadruple émission avec bien moins de contraintes d’appariement. Imaginez qu'on se soit limité à quatre ports d'émission : il aurait fallu mettre plusieurs unités sur le même port d'émission. En conséquence, il y aurait eu des combinaisons interdites, comme émettre deux instructions entières + un branchement + un accès mémoire ; ou deux instructions entières + une opération flottante + un accès mémoire. De tells appariement interdits sont possibles en utilisant les six ports d'émission. ==L'émission multiple des accès mémoire== Les premiers processeurs superscalaires ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles. Par contre, c'est autre chose sur les processeurs superscalaires larges. Vu qu'ils travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares. L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. La ''load/store queue'' reste une structure unique, mais on peut lui rajouter des ports. Et à ce peit jeu, il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire. [[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]] 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. Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre. Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Le Pentium 4 était dans ce cas, mais il était un peu seul dans sa catégorie. Un cas similaire est celui où l'unité mémoire est alimentée par une fenêtre de micro-opération mélangeant instructions mémoire et entières. Les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. Dans ce cas, l'émission multiple est rarement implémentée. Le cas suivant a une file pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique. Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui pouvaient seulement émettre une lecture en même temps qu'une écriture. Ils avaient deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse. [[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]] Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture demande d'utiliser une file d'écriture multiport, ce qui est assez compliqué. Implémenter des lectures en plus est plus simple : il suffit de rajouter une seconde file de µops LOAD et une unité de calcul, alimentées via un second port d'émission. Il est aussi possible de rajouter un second port de lecture au cache, mais ce n'est pas obligatoire. Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse. De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports de lecture et d'écriture car il y a plus de lecture que d'écritures. Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. : Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse. [[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]] Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettent plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail. Pour résumer, l'évolution des CPU superscalaires a été la suivante : * pipeline mémorie fusionné avec le pipeline entier ; * séparation du pipeline mémoire du reste, avec émission parallèle des accès mémoire ; * émission multiple, mais sans dupliquer les ports du cache ; * émission multiple, avec émission simultanée d'une lecture et d’une écriture dans le cache ; * émission multiple avec plusieurs accès en lecture au cache. ==Les optimisations des processeurs superscalaires larges== Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul. ===L'étape de chargement d'un CPU superscalaire large=== Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 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 autoaligné.]] Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements ! [[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]] Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez 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 microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total). Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois 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 microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement. Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations. ===La micro-fusion des processeurs Intel et AMD=== La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''. Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''. Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout. La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Le parallélisme mémoire | prevText=Le parallélisme mémoire | next=Exemples de microarchitectures CPU : le cas du x86 | nextText=Exemples de microarchitectures CPU : le cas du x86 }} </noinclude> rx17j0y3vc3otmgdduw0eu0f1pcodu1 772712 772711 2026-09-21T18:15:20Z Mewtow 31375 /* L'unité d'émission d'un processeur superscalaire */ 772712 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 tout premiers processeurs superscalaires pouvaient émettre deux µops en même temps, ce qui portait le nom de '''double émission'''. Mais le nombre a rapidement augmenté et les processeurs modernes 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 des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. Il y a donc des paires d'instructions autorisées et des paires interdites. En pratique, ces contraintes d’appariement ont peu d’impact. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Deux branchements consécutifs sont quant à eux plus fréquents, mais exécuter deux branchements en même temps pose un problème : que faire quand les deux sont pris ? Deux accès mémoire consécutifs ou proches est assez fréquent, et dupliquer l'unité mémoire est compliqué, bien que possible. Dupliquer des ALU entières est donc la priorité, suivi par la duplication des unités mémoire, pour terminer par le multiplieur et la FPU. ==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. Par exemple, l'unité de chargement charge deux fois plus d'instructions, mais n'est pas modifiée en profondeur. L'unité d'émission et les circuits d'exécution dans le désordre sont eux aussi altérés. Les unités de calcul sont souvent dupliquées, sauf sur les tout premiers CPU superscalaires. {|class="wikitable" |- ! rowspan="2" | Processeur sans émission multiple | rowspan="2" | Chargement | rowspan="2" | Décodage | rowspan="2" | Renommage | rowspan="2" | Émission | Exécution / ALU | rowspan="2" | ''Commit''/ROB |- | Exécution / ALU |- ! rowspan="4" | Processeur superscalaire | rowspan="4" | Chargement | rowspan="2" | Décodage | rowspan="4" | Renommage | rowspan="4" | Émission | Exécution / ALU | rowspan="4" | ''Commit''/ROB |- | Exécution / ALU |- | rowspan="2" | Décodage | Exécution / ALU |- | Exécution / ALU |} Voyons ce qu'il en est dans le détail. ===Les unités de calcul d'un processeur superscalaire=== Émettre N instructions simultanément demande implicitement d'exécuter N instructions par cycle, voire plus si des instructions multicycles viennent mettre leur grain de sel. 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 basique, avec seulement des ALU entières, peut se le permettre. Du moins, à condition de ne pas dupliquer l'unité mémoire, ce qui fait qu'il a pas mal de contraintes d'appariement. Par exemple, un processeur RISC basique avec seulement une ALU entière et un ''barrel shifter'' peut être rendu superscalaire en dupliquant les deux circuits. Le problème, est que cette solution marche assez mal dès que le processeur contient des circuits multiplieurs et/ou une FPU. Par exemple, un processeur à double émission devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, etc. 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 ! Par contre, l'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission. Un autre cas extrême est celui où aucune unité de calcul n'est dupliquée. Pour comprendre comment c'est possible rappelez-vous qu'un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et éventuellement une unité de branchement. Cela suffit pour créer un processeur quadruple émission, très limité. Un tel processeur peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire. Le problème est qu'il est rare que l'on ait à exécuter en même temps un calcul entier, un calcul flottant, un accès mémoire et un branchement. En pratique, les seules combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser la quadruple émission n'est donc pas une bonne idée. 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. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant. ===Le chargement de plusieurs instructions consécutives=== Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large. Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence. Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas. Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement non-pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc. [[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]] ===Le décodage parallèle des instructions=== Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits. Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur. Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! ===L'unité d'émission d'un processeur superscalaire=== La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. 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 4 instructions, compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément deux 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. 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. : 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 microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle. 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=== Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement. 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. ==L'évolution historique des processeurs superscalaires== Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs triple émission permettent eux d’émettre trois instructions à la fois. Les tout premiers processeurs superscalaires avaient un budget en transistors très limité, ce qui fait qu'ils ne pouvaient pas être "totalement superscalaires". Par exemple, un processeur double émission ne pouvait pas émettre deux opérations entières en même temps, ni deux opérations flottantes, ni une opération entière avec un accès mémoire. Il y avait des paires d'instructions autorisées et des paires interdites. Les '''contraintes d'appariement''' sur les instructions à émettre en même temps étaient très strictes. L'intérêt de ces contraintes d’appariement est que le processeur était très simple, il n'y avait pas besoin de rajouter des circuits complexes pour rendre le processeur superscalaire. Les CPU superscalaires historiques ne faisaient que mieux utiliser des unités de calcul déjà présentes. Prenons un processeur non-superscalaire simple, illustré ci-dessous. Il contient : une unité de chargement, un décodeur, une unité d'émission, une ALU, une FPU, les bancs de registre entier et flottants, et une unité mémoire. Nous avons placé l'unité mémoire à la suite de l'ALU entière, car ce sera important pour les explications qui vont suivre. [[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]] Les processeurs superscalaires historiques modifiaient assez peu le schéma précédent. L'unité de chargement était adaptée pour lire plusieurs instructions depuis la mémoire, il y avait un seul décodeur, l'unité d'émission était assez peu modifiée. Mais une chose était certaine : aucune unité de calcul n'était dupliquée, le banc de registre n'était presque pas modifié. A la rigueur, gérer la triple 'émission avec les branchements demandait parfois d'ajouter des ports de lecture sur le banc de registre généraux, pas plus. ===La double émission entière-flottante=== Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est d'émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Le processeur a donc deux pipelines séparés : un pour les micro-opérations entières, un autre pour les micro-opérations flottantes. On parle alors de '''double émission entière-flottante'''. {|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, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité. La plupart des premiers processeurs superscalaires étaient de ce type. Les exemples les plus notables sont les processeurs POWER 1 et ses dérivés comme le ''RISC Single Chip''. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle. [[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]] La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisations visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate]. ===L'émission parallèle des branchements=== Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur. 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 modifier le banc de registres. Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. [[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]] Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications et les divisions ? Pour étudier ces questions, nous allons prendre le cas d'un processeur capable d’émettre trois opérations en même temps : deux opérations entières, et une flottante, potentiellement d'autres. {|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 |} 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|Emission multiple des opérations entières, implémentation courante]] Pour les branchements, deux solutions sont possibles. Avec la première, il y a émission parallèle des branchements, à savoir qu'il est possible d'émettre en branchement en parallèle de deux-trois instructions entières. Et cela impose la présence d'une unité de branchement séparée, avec son port d'émission. Avec la seconde, les branchements sont traités comme des instructions entières. Soit les branchements sont exécutés dans une ALU entière, soit il y a une unité de branchement reliée à un port d'émission pour les opérations entières. [[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Emission série et parallèle des branchements]] Avec la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales, sur les processeurs superscalaires historiques. En clair, le processeur peut émettre 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. De plus, 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. Ce compromis change cependant quand on passe à la quadruple émission. {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 |- | * ALU entière * Multiplieur | * ALU entière * Barrel shifter * Unité de branchement * Unité mémoire | FPU |- | * Additions, soustractions, opérations bit à bit, etc. * Multiplication | * Additions, soustractions, opérations bit à bit, etc. * Décalages et rotations * Branchements * Accès mémoire |Opération flottante |} ===Les CPU superscalaires à plusieurs unités spécialisées=== Pour la triple émission, deux opérations entières + une flottante est l'idéal. Même les processeurs modernes à triple émission restent sur cette "norme". Mais arrivé à la quadruple émission, les CPU superscalaires ont évolués dans des directions différentes, chacun faisant à sa sauce. La raison est que la quadruple émission permet de nombreuses possibilités, mais la plupart ne sont pas intéressantes. Prenons un CPU superscalaire avec une ALU entière, une FPU, une unité de branchement, et une unité mémoire. Il est possible de créer un CPU quadruple émission avec ces unités, mais le gain en performance n'en vaudrait pas la peine. Il est rare qu'un bloc de 4 instructions contienne exactement une opération entière, une flottante, un branchement et un accès mémoire. En pratique, les seules combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. En conséquence, une fois arrivé à la quadruple émission, les processeurs superscalaires n'ont pas le choix que de rajouter des unités de calcul, voire des unités mémoire. Et ils ont privilégié l'ajout d'ALU entières. La raison est que les programmes informatiques utilisent beaucoup plus d'additions/soustractions que le reste. Il y a environ 5 opérations entières de base pour une multiplication entière, le ratio est le même quand on remplace la multiplication par un branchement ou un accès mémoire. En conséquence, les ALU entières ont été dupliquées en premier, et dans de nombreux exemplaires. Reste à voir comment a été dupliqué le reste. Les multiplieurs n'ont pas été dupliqués, sauf sur les processeurs superscalaires très larges. La raison est qu'il est rare de devoir exécuter deux multiplications à la fois. Et c'est la même chose pour le ''barrel shifter''. Deux branchements consécutifs sont quant à eux plus fréquents, c'est même assez courant. Aussi, les unités de branchement ont été dupliquées en second lieu, après les ALU entières. Avoir plusieurs accès mémoire proches est assez courant, surtout sur les CPU de type CISC. Aussi, les unités mémoire ont été dupliquées en troisième lieu. Pour résumer, dupliquer des ALU entières est donc la priorité, suivi par la duplication des unités de branchement, puis celle des accès mémoire, pour terminer par le multiplieur et le ''barrel shifter''. La FPU est un peu à part. Une autre tendance a été de séparer l'unité mémoire des ALU entières, ce qui a permis l''''émission parallèle des µops mémoire'''. Comme son nom l'indique, l'idée est d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière. La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple. La '''double émission flottante''' a été utilisée sur les processeurs Alpha. L'idée est d'émettre deux opérations flottantes à la fois. Sauf qu'une implémentation naïve demanderait de dupliquer la FPU, ce qui aurait un cout en circuit rédhibitoire. Pour rappel, les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant. L'idée est d'avoir des ports d'émission séparés pour l'additionneur flottant et le multiplieur flottant. Le processeur peut alors émettre une addition flottante en même temps qu'une multiplication flottante. Les autres circuits de calcul flottant sont répartis sur ces deux ports d'émission flottants. L'avantage est que cela se marie bien avec l'usage d'une fenêtre d'instruction séparée pour les opérations flottantes. La fenêtre d'instruction a alors deux ports séparés, au lieu d'un seul. Rajouter un second port d'émission flottant n'est pas trop un problème, car le cout lié à l'ajout d'un port n'est pas linéaire. Passer de un port à deux a un cout tolérable, bien plus que de passer de 3 ports à 4 ou de 4 à 5. Il est possible de dupliquer l'additionneur et le multiplieur flottant, ce qui permet d'émettre deux additions et multiplications flottantes en même temps. C'est ce qui est fait sur les processeurs AMD d'architecture Zen 1 et 2. Ils ont deux additionneurs flottants par cœur, deux multiplieurs flottants, chacun avec leur propre port d'émission. Les performances en calcul flottant sont assez impressionnantes pour un processeur de l'époque. [[File:ZEN - émission multiple flottante.png|centre|vignette|upright=2|Microarchitecture Zen 1 d'AMD.]] ==Les ports d'émission et de décodage== Les ports d'émission et de décodage sont des ressources matérielles comme les autres. Dans cette section, nous allons parler de concepts liés aux ports d'émission et de décodage. Nous allons notamment parler du partage des ports d'émission, ainsi que des relations numériques entre ces deux types de ports. ===Le partage des ports d'émission et la ''port contention''=== Pour le moment, nous n'avons vu que des cas où un port d'émission est relié soit à des ALU entières, soit à des FPU, pas aux deux. Mais il existe des processeurs où un même port d'émission alimente à la fois une ALU entière et une FPU. Par exemple, on peut relier un additionneur flottant sur le même port qu'une ALU entière. Il faut noter que cela implique une fenêtre d'instruction centralisée, capable de mettre en attente micro-opérations entières et flottantes. Un exemple est celui des processeurs Core 2 Duo. Ils disposent de 6 ports d'émission, dont 3 ports dédiés à l'unité mémoire. Les 3 ports restants alimentent chacun : une ALU entière, un circuit de calcul flottant et une unité de calcul SSE (une unité de calcul SIMD, qu'on abordera dans quelques chapitres). * Le premier port alimente une ALU entière simple et un multiplieur/diviseur flottant. * Le second alimente une ALU entière, un multiplieur entier et un additionneur flottant. * Le troisième alimente une ALU entière, sans circuit flottant dédié. Dans le même genre, il est possible de partager un port d'émission entre l'unité mémoire et une ALU entière. Cela permet d'utiliser l'ALU entière pour les calculs d'adresse, ce qui évite d'avoir à utiliser une unité de calcul d'adresse distincte. Un exemple est celui du processeur superscalaire double émission Power PC 440. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. L'organisation en question permet soit d'émettre un accès mémoire en même temps qu'une opération entière, soit d'émettre deux opérations entières simples, soit d’émettre une multiplication et une addition/soustraction/comparaison. Une organisation simple, mais très efficace ! [[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]] ===La séparation entre largeur de décodage et d'émission=== Sur les processeurs superscalaires larges, le processeur peut émettre plus d'instructions qu'il peut en décoder. Plus haut, j'ai dit que l'unité d'émission avait parfois plus de ports d'émission que de ports de décodage. La raison est assez simple, et elle s'explique assez bien avec un exemple. Prenons un processeur avec trois ALU entières, une FPU, une unité de branchement et une unité mémoire. Chacune a son propre port d'émission, ce qui fait que le CPU est capable d'émettre 6 µops. Du moins, c'est la théorie, car les conditions pour sont assez rares. Il faut qu'en chargeant un paquet de 6 instructions, il y ait exactement trois instructions entières, une flottante, un branchement et un accès mémoire. Si le paquet contient 5 instructions entières et un branchement, seules quatre instructions sont émises. Dans cet exemple, dans la grande majorité des cas, seuls 4 ports seront utilisés en moyenne et deux seront sous-utilisés. Il s'agit là d'une moyenne, qui dépend du code, et des instructions exactes à exécuter. Cependant, cette moyenne nous donne un moyen d'économiser beaucoup de circuits pour un cout en performance mineur. L'idée est de ne charger que des blocs de 4 instructions, mais d'avoir 6 ports d'émission. Le processeur charge et décode 4 instructions, les quatre instructions sont envoyées sur les ports d'émission adéquat. Le cout en transistors est alors situé entre celui d'un CPU quadruple émission et sextuple émission. Et les performances sont aussi entre les deux. Pour les performances, c'est parce que cela permet d'avoir un CPU quadruple émission avec bien moins de contraintes d’appariement. Imaginez qu'on se soit limité à quatre ports d'émission : il aurait fallu mettre plusieurs unités sur le même port d'émission. En conséquence, il y aurait eu des combinaisons interdites, comme émettre deux instructions entières + un branchement + un accès mémoire ; ou deux instructions entières + une opération flottante + un accès mémoire. De tells appariement interdits sont possibles en utilisant les six ports d'émission. ==L'émission multiple des accès mémoire== Les premiers processeurs superscalaires ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles. Par contre, c'est autre chose sur les processeurs superscalaires larges. Vu qu'ils travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares. L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. La ''load/store queue'' reste une structure unique, mais on peut lui rajouter des ports. Et à ce peit jeu, il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire. [[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]] 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. Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre. Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Le Pentium 4 était dans ce cas, mais il était un peu seul dans sa catégorie. Un cas similaire est celui où l'unité mémoire est alimentée par une fenêtre de micro-opération mélangeant instructions mémoire et entières. Les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. Dans ce cas, l'émission multiple est rarement implémentée. Le cas suivant a une file pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique. Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui pouvaient seulement émettre une lecture en même temps qu'une écriture. Ils avaient deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse. [[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]] Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture demande d'utiliser une file d'écriture multiport, ce qui est assez compliqué. Implémenter des lectures en plus est plus simple : il suffit de rajouter une seconde file de µops LOAD et une unité de calcul, alimentées via un second port d'émission. Il est aussi possible de rajouter un second port de lecture au cache, mais ce n'est pas obligatoire. Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse. De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports de lecture et d'écriture car il y a plus de lecture que d'écritures. Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. : Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse. [[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]] Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettent plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail. Pour résumer, l'évolution des CPU superscalaires a été la suivante : * pipeline mémorie fusionné avec le pipeline entier ; * séparation du pipeline mémoire du reste, avec émission parallèle des accès mémoire ; * émission multiple, mais sans dupliquer les ports du cache ; * émission multiple, avec émission simultanée d'une lecture et d’une écriture dans le cache ; * émission multiple avec plusieurs accès en lecture au cache. ==Les optimisations des processeurs superscalaires larges== Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul. ===L'étape de chargement d'un CPU superscalaire large=== Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 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 autoaligné.]] Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements ! [[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]] Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez 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 microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total). Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois 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 microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement. Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations. ===La micro-fusion des processeurs Intel et AMD=== La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''. Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''. Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout. La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Le parallélisme mémoire | prevText=Le parallélisme mémoire | next=Exemples de microarchitectures CPU : le cas du x86 | nextText=Exemples de microarchitectures CPU : le cas du x86 }} </noinclude> 262lviiib1hv7vqqfvvwwk7no3aj7si 772713 772712 2026-09-21T18:21:59Z Mewtow 31375 /* Les contraintes d’appariement */ 772713 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 tout premiers processeurs superscalaires pouvaient émettre deux µops en même temps, ce qui portait le nom de '''double émission'''. Mais le nombre a rapidement augmenté et les processeurs modernes 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. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps. 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. 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. Deux branchements consécutifs sont quant à eux plus fréquents, mais exécuter deux branchements en même temps pose un problème : que faire quand les deux sont pris ? Deux accès mémoire consécutifs est assez rare. La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs sont systématiquement supportées, car dupliquer des ALU 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 et 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'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. Par exemple, l'unité de chargement charge deux fois plus d'instructions, mais n'est pas modifiée en profondeur. L'unité d'émission et les circuits d'exécution dans le désordre sont eux aussi altérés. Les unités de calcul sont souvent dupliquées, sauf sur les tout premiers CPU superscalaires. {|class="wikitable" |- ! rowspan="2" | Processeur sans émission multiple | rowspan="2" | Chargement | rowspan="2" | Décodage | rowspan="2" | Renommage | rowspan="2" | Émission | Exécution / ALU | rowspan="2" | ''Commit''/ROB |- | Exécution / ALU |- ! rowspan="4" | Processeur superscalaire | rowspan="4" | Chargement | rowspan="2" | Décodage | rowspan="4" | Renommage | rowspan="4" | Émission | Exécution / ALU | rowspan="4" | ''Commit''/ROB |- | Exécution / ALU |- | rowspan="2" | Décodage | Exécution / ALU |- | Exécution / ALU |} Voyons ce qu'il en est dans le détail. ===Les unités de calcul d'un processeur superscalaire=== Émettre N instructions simultanément demande implicitement d'exécuter N instructions par cycle, voire plus si des instructions multicycles viennent mettre leur grain de sel. 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 basique, avec seulement des ALU entières, peut se le permettre. Du moins, à condition de ne pas dupliquer l'unité mémoire, ce qui fait qu'il a pas mal de contraintes d'appariement. Par exemple, un processeur RISC basique avec seulement une ALU entière et un ''barrel shifter'' peut être rendu superscalaire en dupliquant les deux circuits. Le problème, est que cette solution marche assez mal dès que le processeur contient des circuits multiplieurs et/ou une FPU. Par exemple, un processeur à double émission devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, etc. 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 ! Par contre, l'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission. Un autre cas extrême est celui où aucune unité de calcul n'est dupliquée. Pour comprendre comment c'est possible rappelez-vous qu'un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et éventuellement une unité de branchement. Cela suffit pour créer un processeur quadruple émission, très limité. Un tel processeur peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire. Le problème est qu'il est rare que l'on ait à exécuter en même temps un calcul entier, un calcul flottant, un accès mémoire et un branchement. En pratique, les seules combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser la quadruple émission n'est donc pas une bonne idée. 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. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant. ===Le chargement de plusieurs instructions consécutives=== Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large. Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence. Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas. Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement non-pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc. [[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]] ===Le décodage parallèle des instructions=== Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits. Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur. Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! ===L'unité d'émission d'un processeur superscalaire=== La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. 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 4 instructions, compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément deux 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. 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. : 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 microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle. 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=== Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement. 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. ==L'évolution historique des processeurs superscalaires== Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs triple émission permettent eux d’émettre trois instructions à la fois. Les tout premiers processeurs superscalaires avaient un budget en transistors très limité, ce qui fait qu'ils ne pouvaient pas être "totalement superscalaires". Par exemple, un processeur double émission ne pouvait pas émettre deux opérations entières en même temps, ni deux opérations flottantes, ni une opération entière avec un accès mémoire. Il y avait des paires d'instructions autorisées et des paires interdites. Les '''contraintes d'appariement''' sur les instructions à émettre en même temps étaient très strictes. L'intérêt de ces contraintes d’appariement est que le processeur était très simple, il n'y avait pas besoin de rajouter des circuits complexes pour rendre le processeur superscalaire. Les CPU superscalaires historiques ne faisaient que mieux utiliser des unités de calcul déjà présentes. Prenons un processeur non-superscalaire simple, illustré ci-dessous. Il contient : une unité de chargement, un décodeur, une unité d'émission, une ALU, une FPU, les bancs de registre entier et flottants, et une unité mémoire. Nous avons placé l'unité mémoire à la suite de l'ALU entière, car ce sera important pour les explications qui vont suivre. [[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]] Les processeurs superscalaires historiques modifiaient assez peu le schéma précédent. L'unité de chargement était adaptée pour lire plusieurs instructions depuis la mémoire, il y avait un seul décodeur, l'unité d'émission était assez peu modifiée. Mais une chose était certaine : aucune unité de calcul n'était dupliquée, le banc de registre n'était presque pas modifié. A la rigueur, gérer la triple 'émission avec les branchements demandait parfois d'ajouter des ports de lecture sur le banc de registre généraux, pas plus. ===La double émission entière-flottante=== Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est d'émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Le processeur a donc deux pipelines séparés : un pour les micro-opérations entières, un autre pour les micro-opérations flottantes. On parle alors de '''double émission entière-flottante'''. {|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, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité. La plupart des premiers processeurs superscalaires étaient de ce type. Les exemples les plus notables sont les processeurs POWER 1 et ses dérivés comme le ''RISC Single Chip''. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle. [[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]] La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisations visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate]. ===L'émission parallèle des branchements=== Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur. 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 modifier le banc de registres. Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. [[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]] Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications et les divisions ? Pour étudier ces questions, nous allons prendre le cas d'un processeur capable d’émettre trois opérations en même temps : deux opérations entières, et une flottante, potentiellement d'autres. {|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 |} 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|Emission multiple des opérations entières, implémentation courante]] Pour les branchements, deux solutions sont possibles. Avec la première, il y a émission parallèle des branchements, à savoir qu'il est possible d'émettre en branchement en parallèle de deux-trois instructions entières. Et cela impose la présence d'une unité de branchement séparée, avec son port d'émission. Avec la seconde, les branchements sont traités comme des instructions entières. Soit les branchements sont exécutés dans une ALU entière, soit il y a une unité de branchement reliée à un port d'émission pour les opérations entières. [[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Emission série et parallèle des branchements]] Avec la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales, sur les processeurs superscalaires historiques. En clair, le processeur peut émettre 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. De plus, 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. Ce compromis change cependant quand on passe à la quadruple émission. {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 |- | * ALU entière * Multiplieur | * ALU entière * Barrel shifter * Unité de branchement * Unité mémoire | FPU |- | * Additions, soustractions, opérations bit à bit, etc. * Multiplication | * Additions, soustractions, opérations bit à bit, etc. * Décalages et rotations * Branchements * Accès mémoire |Opération flottante |} ===Les CPU superscalaires à plusieurs unités spécialisées=== Pour la triple émission, deux opérations entières + une flottante est l'idéal. Même les processeurs modernes à triple émission restent sur cette "norme". Mais arrivé à la quadruple émission, les CPU superscalaires ont évolués dans des directions différentes, chacun faisant à sa sauce. La raison est que la quadruple émission permet de nombreuses possibilités, mais la plupart ne sont pas intéressantes. Prenons un CPU superscalaire avec une ALU entière, une FPU, une unité de branchement, et une unité mémoire. Il est possible de créer un CPU quadruple émission avec ces unités, mais le gain en performance n'en vaudrait pas la peine. Il est rare qu'un bloc de 4 instructions contienne exactement une opération entière, une flottante, un branchement et un accès mémoire. En pratique, les seules combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. En conséquence, une fois arrivé à la quadruple émission, les processeurs superscalaires n'ont pas le choix que de rajouter des unités de calcul, voire des unités mémoire. Et ils ont privilégié l'ajout d'ALU entières. La raison est que les programmes informatiques utilisent beaucoup plus d'additions/soustractions que le reste. Il y a environ 5 opérations entières de base pour une multiplication entière, le ratio est le même quand on remplace la multiplication par un branchement ou un accès mémoire. En conséquence, les ALU entières ont été dupliquées en premier, et dans de nombreux exemplaires. Reste à voir comment a été dupliqué le reste. Les multiplieurs n'ont pas été dupliqués, sauf sur les processeurs superscalaires très larges. La raison est qu'il est rare de devoir exécuter deux multiplications à la fois. Et c'est la même chose pour le ''barrel shifter''. Deux branchements consécutifs sont quant à eux plus fréquents, c'est même assez courant. Aussi, les unités de branchement ont été dupliquées en second lieu, après les ALU entières. Avoir plusieurs accès mémoire proches est assez courant, surtout sur les CPU de type CISC. Aussi, les unités mémoire ont été dupliquées en troisième lieu. Pour résumer, dupliquer des ALU entières est donc la priorité, suivi par la duplication des unités de branchement, puis celle des accès mémoire, pour terminer par le multiplieur et le ''barrel shifter''. La FPU est un peu à part. Une autre tendance a été de séparer l'unité mémoire des ALU entières, ce qui a permis l''''émission parallèle des µops mémoire'''. Comme son nom l'indique, l'idée est d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière. La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple. La '''double émission flottante''' a été utilisée sur les processeurs Alpha. L'idée est d'émettre deux opérations flottantes à la fois. Sauf qu'une implémentation naïve demanderait de dupliquer la FPU, ce qui aurait un cout en circuit rédhibitoire. Pour rappel, les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant. L'idée est d'avoir des ports d'émission séparés pour l'additionneur flottant et le multiplieur flottant. Le processeur peut alors émettre une addition flottante en même temps qu'une multiplication flottante. Les autres circuits de calcul flottant sont répartis sur ces deux ports d'émission flottants. L'avantage est que cela se marie bien avec l'usage d'une fenêtre d'instruction séparée pour les opérations flottantes. La fenêtre d'instruction a alors deux ports séparés, au lieu d'un seul. Rajouter un second port d'émission flottant n'est pas trop un problème, car le cout lié à l'ajout d'un port n'est pas linéaire. Passer de un port à deux a un cout tolérable, bien plus que de passer de 3 ports à 4 ou de 4 à 5. Il est possible de dupliquer l'additionneur et le multiplieur flottant, ce qui permet d'émettre deux additions et multiplications flottantes en même temps. C'est ce qui est fait sur les processeurs AMD d'architecture Zen 1 et 2. Ils ont deux additionneurs flottants par cœur, deux multiplieurs flottants, chacun avec leur propre port d'émission. Les performances en calcul flottant sont assez impressionnantes pour un processeur de l'époque. [[File:ZEN - émission multiple flottante.png|centre|vignette|upright=2|Microarchitecture Zen 1 d'AMD.]] ==Les ports d'émission et de décodage== Les ports d'émission et de décodage sont des ressources matérielles comme les autres. Dans cette section, nous allons parler de concepts liés aux ports d'émission et de décodage. Nous allons notamment parler du partage des ports d'émission, ainsi que des relations numériques entre ces deux types de ports. ===Le partage des ports d'émission et la ''port contention''=== Pour le moment, nous n'avons vu que des cas où un port d'émission est relié soit à des ALU entières, soit à des FPU, pas aux deux. Mais il existe des processeurs où un même port d'émission alimente à la fois une ALU entière et une FPU. Par exemple, on peut relier un additionneur flottant sur le même port qu'une ALU entière. Il faut noter que cela implique une fenêtre d'instruction centralisée, capable de mettre en attente micro-opérations entières et flottantes. Un exemple est celui des processeurs Core 2 Duo. Ils disposent de 6 ports d'émission, dont 3 ports dédiés à l'unité mémoire. Les 3 ports restants alimentent chacun : une ALU entière, un circuit de calcul flottant et une unité de calcul SSE (une unité de calcul SIMD, qu'on abordera dans quelques chapitres). * Le premier port alimente une ALU entière simple et un multiplieur/diviseur flottant. * Le second alimente une ALU entière, un multiplieur entier et un additionneur flottant. * Le troisième alimente une ALU entière, sans circuit flottant dédié. Dans le même genre, il est possible de partager un port d'émission entre l'unité mémoire et une ALU entière. Cela permet d'utiliser l'ALU entière pour les calculs d'adresse, ce qui évite d'avoir à utiliser une unité de calcul d'adresse distincte. Un exemple est celui du processeur superscalaire double émission Power PC 440. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. L'organisation en question permet soit d'émettre un accès mémoire en même temps qu'une opération entière, soit d'émettre deux opérations entières simples, soit d’émettre une multiplication et une addition/soustraction/comparaison. Une organisation simple, mais très efficace ! [[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]] ===La séparation entre largeur de décodage et d'émission=== Sur les processeurs superscalaires larges, le processeur peut émettre plus d'instructions qu'il peut en décoder. Plus haut, j'ai dit que l'unité d'émission avait parfois plus de ports d'émission que de ports de décodage. La raison est assez simple, et elle s'explique assez bien avec un exemple. Prenons un processeur avec trois ALU entières, une FPU, une unité de branchement et une unité mémoire. Chacune a son propre port d'émission, ce qui fait que le CPU est capable d'émettre 6 µops. Du moins, c'est la théorie, car les conditions pour sont assez rares. Il faut qu'en chargeant un paquet de 6 instructions, il y ait exactement trois instructions entières, une flottante, un branchement et un accès mémoire. Si le paquet contient 5 instructions entières et un branchement, seules quatre instructions sont émises. Dans cet exemple, dans la grande majorité des cas, seuls 4 ports seront utilisés en moyenne et deux seront sous-utilisés. Il s'agit là d'une moyenne, qui dépend du code, et des instructions exactes à exécuter. Cependant, cette moyenne nous donne un moyen d'économiser beaucoup de circuits pour un cout en performance mineur. L'idée est de ne charger que des blocs de 4 instructions, mais d'avoir 6 ports d'émission. Le processeur charge et décode 4 instructions, les quatre instructions sont envoyées sur les ports d'émission adéquat. Le cout en transistors est alors situé entre celui d'un CPU quadruple émission et sextuple émission. Et les performances sont aussi entre les deux. Pour les performances, c'est parce que cela permet d'avoir un CPU quadruple émission avec bien moins de contraintes d’appariement. Imaginez qu'on se soit limité à quatre ports d'émission : il aurait fallu mettre plusieurs unités sur le même port d'émission. En conséquence, il y aurait eu des combinaisons interdites, comme émettre deux instructions entières + un branchement + un accès mémoire ; ou deux instructions entières + une opération flottante + un accès mémoire. De tells appariement interdits sont possibles en utilisant les six ports d'émission. ==L'émission multiple des accès mémoire== Les premiers processeurs superscalaires ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles. Par contre, c'est autre chose sur les processeurs superscalaires larges. Vu qu'ils travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares. L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. La ''load/store queue'' reste une structure unique, mais on peut lui rajouter des ports. Et à ce peit jeu, il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire. [[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]] 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. Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre. Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Le Pentium 4 était dans ce cas, mais il était un peu seul dans sa catégorie. Un cas similaire est celui où l'unité mémoire est alimentée par une fenêtre de micro-opération mélangeant instructions mémoire et entières. Les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. Dans ce cas, l'émission multiple est rarement implémentée. Le cas suivant a une file pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique. Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui pouvaient seulement émettre une lecture en même temps qu'une écriture. Ils avaient deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse. [[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]] Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture demande d'utiliser une file d'écriture multiport, ce qui est assez compliqué. Implémenter des lectures en plus est plus simple : il suffit de rajouter une seconde file de µops LOAD et une unité de calcul, alimentées via un second port d'émission. Il est aussi possible de rajouter un second port de lecture au cache, mais ce n'est pas obligatoire. Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse. De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports de lecture et d'écriture car il y a plus de lecture que d'écritures. Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. : Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse. [[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]] Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettent plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail. Pour résumer, l'évolution des CPU superscalaires a été la suivante : * pipeline mémorie fusionné avec le pipeline entier ; * séparation du pipeline mémoire du reste, avec émission parallèle des accès mémoire ; * émission multiple, mais sans dupliquer les ports du cache ; * émission multiple, avec émission simultanée d'une lecture et d’une écriture dans le cache ; * émission multiple avec plusieurs accès en lecture au cache. ==Les optimisations des processeurs superscalaires larges== Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul. ===L'étape de chargement d'un CPU superscalaire large=== Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 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 autoaligné.]] Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements ! [[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]] Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez 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 microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total). Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois 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 microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement. Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations. ===La micro-fusion des processeurs Intel et AMD=== La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''. Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''. Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout. La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Le parallélisme mémoire | prevText=Le parallélisme mémoire | next=Exemples de microarchitectures CPU : le cas du x86 | nextText=Exemples de microarchitectures CPU : le cas du x86 }} </noinclude> 4wmwwgsa3bpbdb6cojxe1377vg1b1vz 772714 772713 2026-09-21T18:26:21Z Mewtow 31375 /* Les unités de calcul d'un processeur superscalaire */ 772714 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 tout premiers processeurs superscalaires pouvaient émettre deux µops en même temps, ce qui portait le nom de '''double émission'''. Mais le nombre a rapidement augmenté et les processeurs modernes 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. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps. 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. 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. Deux branchements consécutifs sont quant à eux plus fréquents, mais exécuter deux branchements en même temps pose un problème : que faire quand les deux sont pris ? Deux accès mémoire consécutifs est assez rare. La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs sont systématiquement supportées, car dupliquer des ALU 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 et 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'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. Par exemple, l'unité de chargement charge deux fois plus d'instructions, mais n'est pas modifiée en profondeur. L'unité d'émission et les circuits d'exécution dans le désordre sont eux aussi altérés. Les unités de calcul sont souvent dupliquées, sauf sur les tout premiers CPU superscalaires. {|class="wikitable" |- ! rowspan="2" | Processeur sans émission multiple | rowspan="2" | Chargement | rowspan="2" | Décodage | rowspan="2" | Renommage | rowspan="2" | Émission | Exécution / ALU | rowspan="2" | ''Commit''/ROB |- | Exécution / ALU |- ! rowspan="4" | Processeur superscalaire | rowspan="4" | Chargement | rowspan="2" | Décodage | rowspan="4" | Renommage | rowspan="4" | Émission | Exécution / ALU | rowspan="4" | ''Commit''/ROB |- | Exécution / ALU |- | rowspan="2" | Décodage | Exécution / ALU |- | Exécution / ALU |} Voyons ce qu'il en est dans le détail. ===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. 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. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant. 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. ===Le chargement de plusieurs instructions consécutives=== Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large. Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence. Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas. Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement non-pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc. [[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]] ===Le décodage parallèle des instructions=== Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits. Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur. Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! ===L'unité d'émission d'un processeur superscalaire=== La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. 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 4 instructions, compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément deux 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. 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. : 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 microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle. 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=== Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement. 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. ==L'évolution historique des processeurs superscalaires== Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs triple émission permettent eux d’émettre trois instructions à la fois. Les tout premiers processeurs superscalaires avaient un budget en transistors très limité, ce qui fait qu'ils ne pouvaient pas être "totalement superscalaires". Par exemple, un processeur double émission ne pouvait pas émettre deux opérations entières en même temps, ni deux opérations flottantes, ni une opération entière avec un accès mémoire. Il y avait des paires d'instructions autorisées et des paires interdites. Les '''contraintes d'appariement''' sur les instructions à émettre en même temps étaient très strictes. L'intérêt de ces contraintes d’appariement est que le processeur était très simple, il n'y avait pas besoin de rajouter des circuits complexes pour rendre le processeur superscalaire. Les CPU superscalaires historiques ne faisaient que mieux utiliser des unités de calcul déjà présentes. Prenons un processeur non-superscalaire simple, illustré ci-dessous. Il contient : une unité de chargement, un décodeur, une unité d'émission, une ALU, une FPU, les bancs de registre entier et flottants, et une unité mémoire. Nous avons placé l'unité mémoire à la suite de l'ALU entière, car ce sera important pour les explications qui vont suivre. [[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]] Les processeurs superscalaires historiques modifiaient assez peu le schéma précédent. L'unité de chargement était adaptée pour lire plusieurs instructions depuis la mémoire, il y avait un seul décodeur, l'unité d'émission était assez peu modifiée. Mais une chose était certaine : aucune unité de calcul n'était dupliquée, le banc de registre n'était presque pas modifié. A la rigueur, gérer la triple 'émission avec les branchements demandait parfois d'ajouter des ports de lecture sur le banc de registre généraux, pas plus. ===La double émission entière-flottante=== Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est d'émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Le processeur a donc deux pipelines séparés : un pour les micro-opérations entières, un autre pour les micro-opérations flottantes. On parle alors de '''double émission entière-flottante'''. {|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, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité. La plupart des premiers processeurs superscalaires étaient de ce type. Les exemples les plus notables sont les processeurs POWER 1 et ses dérivés comme le ''RISC Single Chip''. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle. [[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]] La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisations visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate]. ===L'émission parallèle des branchements=== Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur. 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 modifier le banc de registres. Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. [[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]] Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications et les divisions ? Pour étudier ces questions, nous allons prendre le cas d'un processeur capable d’émettre trois opérations en même temps : deux opérations entières, et une flottante, potentiellement d'autres. {|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 |} 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|Emission multiple des opérations entières, implémentation courante]] Pour les branchements, deux solutions sont possibles. Avec la première, il y a émission parallèle des branchements, à savoir qu'il est possible d'émettre en branchement en parallèle de deux-trois instructions entières. Et cela impose la présence d'une unité de branchement séparée, avec son port d'émission. Avec la seconde, les branchements sont traités comme des instructions entières. Soit les branchements sont exécutés dans une ALU entière, soit il y a une unité de branchement reliée à un port d'émission pour les opérations entières. [[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Emission série et parallèle des branchements]] Avec la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales, sur les processeurs superscalaires historiques. En clair, le processeur peut émettre 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. De plus, 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. Ce compromis change cependant quand on passe à la quadruple émission. {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 |- | * ALU entière * Multiplieur | * ALU entière * Barrel shifter * Unité de branchement * Unité mémoire | FPU |- | * Additions, soustractions, opérations bit à bit, etc. * Multiplication | * Additions, soustractions, opérations bit à bit, etc. * Décalages et rotations * Branchements * Accès mémoire |Opération flottante |} ===Les CPU superscalaires à plusieurs unités spécialisées=== Pour la triple émission, deux opérations entières + une flottante est l'idéal. Même les processeurs modernes à triple émission restent sur cette "norme". Mais arrivé à la quadruple émission, les CPU superscalaires ont évolués dans des directions différentes, chacun faisant à sa sauce. La raison est que la quadruple émission permet de nombreuses possibilités, mais la plupart ne sont pas intéressantes. Prenons un CPU superscalaire avec une ALU entière, une FPU, une unité de branchement, et une unité mémoire. Il est possible de créer un CPU quadruple émission avec ces unités, mais le gain en performance n'en vaudrait pas la peine. Il est rare qu'un bloc de 4 instructions contienne exactement une opération entière, une flottante, un branchement et un accès mémoire. En pratique, les seules combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. En conséquence, une fois arrivé à la quadruple émission, les processeurs superscalaires n'ont pas le choix que de rajouter des unités de calcul, voire des unités mémoire. Et ils ont privilégié l'ajout d'ALU entières. La raison est que les programmes informatiques utilisent beaucoup plus d'additions/soustractions que le reste. Il y a environ 5 opérations entières de base pour une multiplication entière, le ratio est le même quand on remplace la multiplication par un branchement ou un accès mémoire. En conséquence, les ALU entières ont été dupliquées en premier, et dans de nombreux exemplaires. Reste à voir comment a été dupliqué le reste. Les multiplieurs n'ont pas été dupliqués, sauf sur les processeurs superscalaires très larges. La raison est qu'il est rare de devoir exécuter deux multiplications à la fois. Et c'est la même chose pour le ''barrel shifter''. Deux branchements consécutifs sont quant à eux plus fréquents, c'est même assez courant. Aussi, les unités de branchement ont été dupliquées en second lieu, après les ALU entières. Avoir plusieurs accès mémoire proches est assez courant, surtout sur les CPU de type CISC. Aussi, les unités mémoire ont été dupliquées en troisième lieu. Pour résumer, dupliquer des ALU entières est donc la priorité, suivi par la duplication des unités de branchement, puis celle des accès mémoire, pour terminer par le multiplieur et le ''barrel shifter''. La FPU est un peu à part. Une autre tendance a été de séparer l'unité mémoire des ALU entières, ce qui a permis l''''émission parallèle des µops mémoire'''. Comme son nom l'indique, l'idée est d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière. La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple. La '''double émission flottante''' a été utilisée sur les processeurs Alpha. L'idée est d'émettre deux opérations flottantes à la fois. Sauf qu'une implémentation naïve demanderait de dupliquer la FPU, ce qui aurait un cout en circuit rédhibitoire. Pour rappel, les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant. L'idée est d'avoir des ports d'émission séparés pour l'additionneur flottant et le multiplieur flottant. Le processeur peut alors émettre une addition flottante en même temps qu'une multiplication flottante. Les autres circuits de calcul flottant sont répartis sur ces deux ports d'émission flottants. L'avantage est que cela se marie bien avec l'usage d'une fenêtre d'instruction séparée pour les opérations flottantes. La fenêtre d'instruction a alors deux ports séparés, au lieu d'un seul. Rajouter un second port d'émission flottant n'est pas trop un problème, car le cout lié à l'ajout d'un port n'est pas linéaire. Passer de un port à deux a un cout tolérable, bien plus que de passer de 3 ports à 4 ou de 4 à 5. Il est possible de dupliquer l'additionneur et le multiplieur flottant, ce qui permet d'émettre deux additions et multiplications flottantes en même temps. C'est ce qui est fait sur les processeurs AMD d'architecture Zen 1 et 2. Ils ont deux additionneurs flottants par cœur, deux multiplieurs flottants, chacun avec leur propre port d'émission. Les performances en calcul flottant sont assez impressionnantes pour un processeur de l'époque. [[File:ZEN - émission multiple flottante.png|centre|vignette|upright=2|Microarchitecture Zen 1 d'AMD.]] ==Les ports d'émission et de décodage== Les ports d'émission et de décodage sont des ressources matérielles comme les autres. Dans cette section, nous allons parler de concepts liés aux ports d'émission et de décodage. Nous allons notamment parler du partage des ports d'émission, ainsi que des relations numériques entre ces deux types de ports. ===Le partage des ports d'émission et la ''port contention''=== Pour le moment, nous n'avons vu que des cas où un port d'émission est relié soit à des ALU entières, soit à des FPU, pas aux deux. Mais il existe des processeurs où un même port d'émission alimente à la fois une ALU entière et une FPU. Par exemple, on peut relier un additionneur flottant sur le même port qu'une ALU entière. Il faut noter que cela implique une fenêtre d'instruction centralisée, capable de mettre en attente micro-opérations entières et flottantes. Un exemple est celui des processeurs Core 2 Duo. Ils disposent de 6 ports d'émission, dont 3 ports dédiés à l'unité mémoire. Les 3 ports restants alimentent chacun : une ALU entière, un circuit de calcul flottant et une unité de calcul SSE (une unité de calcul SIMD, qu'on abordera dans quelques chapitres). * Le premier port alimente une ALU entière simple et un multiplieur/diviseur flottant. * Le second alimente une ALU entière, un multiplieur entier et un additionneur flottant. * Le troisième alimente une ALU entière, sans circuit flottant dédié. Dans le même genre, il est possible de partager un port d'émission entre l'unité mémoire et une ALU entière. Cela permet d'utiliser l'ALU entière pour les calculs d'adresse, ce qui évite d'avoir à utiliser une unité de calcul d'adresse distincte. Un exemple est celui du processeur superscalaire double émission Power PC 440. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. L'organisation en question permet soit d'émettre un accès mémoire en même temps qu'une opération entière, soit d'émettre deux opérations entières simples, soit d’émettre une multiplication et une addition/soustraction/comparaison. Une organisation simple, mais très efficace ! [[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]] ===La séparation entre largeur de décodage et d'émission=== Sur les processeurs superscalaires larges, le processeur peut émettre plus d'instructions qu'il peut en décoder. Plus haut, j'ai dit que l'unité d'émission avait parfois plus de ports d'émission que de ports de décodage. La raison est assez simple, et elle s'explique assez bien avec un exemple. Prenons un processeur avec trois ALU entières, une FPU, une unité de branchement et une unité mémoire. Chacune a son propre port d'émission, ce qui fait que le CPU est capable d'émettre 6 µops. Du moins, c'est la théorie, car les conditions pour sont assez rares. Il faut qu'en chargeant un paquet de 6 instructions, il y ait exactement trois instructions entières, une flottante, un branchement et un accès mémoire. Si le paquet contient 5 instructions entières et un branchement, seules quatre instructions sont émises. Dans cet exemple, dans la grande majorité des cas, seuls 4 ports seront utilisés en moyenne et deux seront sous-utilisés. Il s'agit là d'une moyenne, qui dépend du code, et des instructions exactes à exécuter. Cependant, cette moyenne nous donne un moyen d'économiser beaucoup de circuits pour un cout en performance mineur. L'idée est de ne charger que des blocs de 4 instructions, mais d'avoir 6 ports d'émission. Le processeur charge et décode 4 instructions, les quatre instructions sont envoyées sur les ports d'émission adéquat. Le cout en transistors est alors situé entre celui d'un CPU quadruple émission et sextuple émission. Et les performances sont aussi entre les deux. Pour les performances, c'est parce que cela permet d'avoir un CPU quadruple émission avec bien moins de contraintes d’appariement. Imaginez qu'on se soit limité à quatre ports d'émission : il aurait fallu mettre plusieurs unités sur le même port d'émission. En conséquence, il y aurait eu des combinaisons interdites, comme émettre deux instructions entières + un branchement + un accès mémoire ; ou deux instructions entières + une opération flottante + un accès mémoire. De tells appariement interdits sont possibles en utilisant les six ports d'émission. ==L'émission multiple des accès mémoire== Les premiers processeurs superscalaires ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles. Par contre, c'est autre chose sur les processeurs superscalaires larges. Vu qu'ils travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares. L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. La ''load/store queue'' reste une structure unique, mais on peut lui rajouter des ports. Et à ce peit jeu, il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire. [[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]] 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. Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre. Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Le Pentium 4 était dans ce cas, mais il était un peu seul dans sa catégorie. Un cas similaire est celui où l'unité mémoire est alimentée par une fenêtre de micro-opération mélangeant instructions mémoire et entières. Les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. Dans ce cas, l'émission multiple est rarement implémentée. Le cas suivant a une file pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique. Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui pouvaient seulement émettre une lecture en même temps qu'une écriture. Ils avaient deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse. [[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]] Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture demande d'utiliser une file d'écriture multiport, ce qui est assez compliqué. Implémenter des lectures en plus est plus simple : il suffit de rajouter une seconde file de µops LOAD et une unité de calcul, alimentées via un second port d'émission. Il est aussi possible de rajouter un second port de lecture au cache, mais ce n'est pas obligatoire. Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse. De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports de lecture et d'écriture car il y a plus de lecture que d'écritures. Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. : Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse. [[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]] Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettent plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail. Pour résumer, l'évolution des CPU superscalaires a été la suivante : * pipeline mémorie fusionné avec le pipeline entier ; * séparation du pipeline mémoire du reste, avec émission parallèle des accès mémoire ; * émission multiple, mais sans dupliquer les ports du cache ; * émission multiple, avec émission simultanée d'une lecture et d’une écriture dans le cache ; * émission multiple avec plusieurs accès en lecture au cache. ==Les optimisations des processeurs superscalaires larges== Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul. ===L'étape de chargement d'un CPU superscalaire large=== Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 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 autoaligné.]] Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements ! [[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]] Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez 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 microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total). Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois 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 microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement. Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations. ===La micro-fusion des processeurs Intel et AMD=== La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''. Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''. Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout. La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Le parallélisme mémoire | prevText=Le parallélisme mémoire | next=Exemples de microarchitectures CPU : le cas du x86 | nextText=Exemples de microarchitectures CPU : le cas du x86 }} </noinclude> rasraa4un8jt4riqsquzhz9vggbdlm9 772715 772714 2026-09-21T18:26:45Z Mewtow 31375 /* Les CPU superscalaires à plusieurs unités spécialisées */ 772715 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 tout premiers processeurs superscalaires pouvaient émettre deux µops en même temps, ce qui portait le nom de '''double émission'''. Mais le nombre a rapidement augmenté et les processeurs modernes 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. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps. 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. 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. Deux branchements consécutifs sont quant à eux plus fréquents, mais exécuter deux branchements en même temps pose un problème : que faire quand les deux sont pris ? Deux accès mémoire consécutifs est assez rare. La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs sont systématiquement supportées, car dupliquer des ALU 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 et 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'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. Par exemple, l'unité de chargement charge deux fois plus d'instructions, mais n'est pas modifiée en profondeur. L'unité d'émission et les circuits d'exécution dans le désordre sont eux aussi altérés. Les unités de calcul sont souvent dupliquées, sauf sur les tout premiers CPU superscalaires. {|class="wikitable" |- ! rowspan="2" | Processeur sans émission multiple | rowspan="2" | Chargement | rowspan="2" | Décodage | rowspan="2" | Renommage | rowspan="2" | Émission | Exécution / ALU | rowspan="2" | ''Commit''/ROB |- | Exécution / ALU |- ! rowspan="4" | Processeur superscalaire | rowspan="4" | Chargement | rowspan="2" | Décodage | rowspan="4" | Renommage | rowspan="4" | Émission | Exécution / ALU | rowspan="4" | ''Commit''/ROB |- | Exécution / ALU |- | rowspan="2" | Décodage | Exécution / ALU |- | Exécution / ALU |} Voyons ce qu'il en est dans le détail. ===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. 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. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant. 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. ===Le chargement de plusieurs instructions consécutives=== Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large. Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence. Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas. Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement non-pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc. [[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]] ===Le décodage parallèle des instructions=== Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits. Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur. Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! ===L'unité d'émission d'un processeur superscalaire=== La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. 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 4 instructions, compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément deux 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. 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. : 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 microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle. 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=== Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement. 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. ==L'évolution historique des processeurs superscalaires== Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs triple émission permettent eux d’émettre trois instructions à la fois. Les tout premiers processeurs superscalaires avaient un budget en transistors très limité, ce qui fait qu'ils ne pouvaient pas être "totalement superscalaires". Par exemple, un processeur double émission ne pouvait pas émettre deux opérations entières en même temps, ni deux opérations flottantes, ni une opération entière avec un accès mémoire. Il y avait des paires d'instructions autorisées et des paires interdites. Les '''contraintes d'appariement''' sur les instructions à émettre en même temps étaient très strictes. L'intérêt de ces contraintes d’appariement est que le processeur était très simple, il n'y avait pas besoin de rajouter des circuits complexes pour rendre le processeur superscalaire. Les CPU superscalaires historiques ne faisaient que mieux utiliser des unités de calcul déjà présentes. Prenons un processeur non-superscalaire simple, illustré ci-dessous. Il contient : une unité de chargement, un décodeur, une unité d'émission, une ALU, une FPU, les bancs de registre entier et flottants, et une unité mémoire. Nous avons placé l'unité mémoire à la suite de l'ALU entière, car ce sera important pour les explications qui vont suivre. [[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]] Les processeurs superscalaires historiques modifiaient assez peu le schéma précédent. L'unité de chargement était adaptée pour lire plusieurs instructions depuis la mémoire, il y avait un seul décodeur, l'unité d'émission était assez peu modifiée. Mais une chose était certaine : aucune unité de calcul n'était dupliquée, le banc de registre n'était presque pas modifié. A la rigueur, gérer la triple 'émission avec les branchements demandait parfois d'ajouter des ports de lecture sur le banc de registre généraux, pas plus. ===La double émission entière-flottante=== Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est d'émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Le processeur a donc deux pipelines séparés : un pour les micro-opérations entières, un autre pour les micro-opérations flottantes. On parle alors de '''double émission entière-flottante'''. {|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, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité. La plupart des premiers processeurs superscalaires étaient de ce type. Les exemples les plus notables sont les processeurs POWER 1 et ses dérivés comme le ''RISC Single Chip''. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle. [[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]] La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisations visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate]. ===L'émission parallèle des branchements=== Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur. 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 modifier le banc de registres. Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. [[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]] Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications et les divisions ? Pour étudier ces questions, nous allons prendre le cas d'un processeur capable d’émettre trois opérations en même temps : deux opérations entières, et une flottante, potentiellement d'autres. {|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 |} 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|Emission multiple des opérations entières, implémentation courante]] Pour les branchements, deux solutions sont possibles. Avec la première, il y a émission parallèle des branchements, à savoir qu'il est possible d'émettre en branchement en parallèle de deux-trois instructions entières. Et cela impose la présence d'une unité de branchement séparée, avec son port d'émission. Avec la seconde, les branchements sont traités comme des instructions entières. Soit les branchements sont exécutés dans une ALU entière, soit il y a une unité de branchement reliée à un port d'émission pour les opérations entières. [[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Emission série et parallèle des branchements]] Avec la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales, sur les processeurs superscalaires historiques. En clair, le processeur peut émettre 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. De plus, 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. Ce compromis change cependant quand on passe à la quadruple émission. {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 |- | * ALU entière * Multiplieur | * ALU entière * Barrel shifter * Unité de branchement * Unité mémoire | FPU |- | * Additions, soustractions, opérations bit à bit, etc. * Multiplication | * Additions, soustractions, opérations bit à bit, etc. * Décalages et rotations * Branchements * Accès mémoire |Opération flottante |} ===Les CPU superscalaires à plusieurs unités spécialisées=== Pour la triple émission, deux opérations entières + une flottante est l'idéal. Même les processeurs modernes à triple émission restent sur cette "norme". Mais arrivé à la quadruple émission, les CPU superscalaires ont évolués dans des directions différentes, chacun faisant à sa sauce. La raison est que la quadruple émission permet de nombreuses possibilités, mais la plupart ne sont pas intéressantes. Prenons un CPU superscalaire avec une ALU entière, une FPU, une unité de branchement, et une unité mémoire. Il est possible de créer un CPU quadruple émission avec ces unités, mais le gain en performance n'en vaudrait pas la peine. Il est rare qu'un bloc de 4 instructions contienne exactement une opération entière, une flottante, un branchement et un accès mémoire. En pratique, les seules combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. En conséquence, une fois arrivé à la quadruple émission, les processeurs superscalaires n'ont pas le choix que de rajouter des unités de calcul, voire des unités mémoire. Et ils ont privilégié l'ajout d'ALU entières. La raison est que les programmes informatiques utilisent beaucoup plus d'additions/soustractions que le reste. Il y a environ 5 opérations entières de base pour une multiplication entière, le ratio est le même quand on remplace la multiplication par un branchement ou un accès mémoire. En conséquence, les ALU entières ont été dupliquées en premier, et dans de nombreux exemplaires. Reste à voir comment a été dupliqué le reste. Les multiplieurs n'ont pas été dupliqués, sauf sur les processeurs superscalaires très larges. La raison est qu'il est rare de devoir exécuter deux multiplications à la fois. Et c'est la même chose pour le ''barrel shifter''. Deux branchements consécutifs sont quant à eux plus fréquents, c'est même assez courant. Aussi, les unités de branchement ont été dupliquées en second lieu, après les ALU entières. Avoir plusieurs accès mémoire proches est assez courant, surtout sur les CPU de type CISC. Aussi, les unités mémoire ont été dupliquées en troisième lieu. Pour résumer, dupliquer des ALU entières est donc la priorité, suivi par la duplication des unités de branchement, puis celle des accès mémoire, pour terminer par le multiplieur et le ''barrel shifter''. La FPU est un peu à part. La '''double émission flottante''' a été utilisée sur les processeurs Alpha. L'idée est d'émettre deux opérations flottantes à la fois. Sauf qu'une implémentation naïve demanderait de dupliquer la FPU, ce qui aurait un cout en circuit rédhibitoire. Pour rappel, les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant. L'idée est d'avoir des ports d'émission séparés pour l'additionneur flottant et le multiplieur flottant. Le processeur peut alors émettre une addition flottante en même temps qu'une multiplication flottante. Les autres circuits de calcul flottant sont répartis sur ces deux ports d'émission flottants. L'avantage est que cela se marie bien avec l'usage d'une fenêtre d'instruction séparée pour les opérations flottantes. La fenêtre d'instruction a alors deux ports séparés, au lieu d'un seul. Rajouter un second port d'émission flottant n'est pas trop un problème, car le cout lié à l'ajout d'un port n'est pas linéaire. Passer de un port à deux a un cout tolérable, bien plus que de passer de 3 ports à 4 ou de 4 à 5. Il est possible de dupliquer l'additionneur et le multiplieur flottant, ce qui permet d'émettre deux additions et multiplications flottantes en même temps. C'est ce qui est fait sur les processeurs AMD d'architecture Zen 1 et 2. Ils ont deux additionneurs flottants par cœur, deux multiplieurs flottants, chacun avec leur propre port d'émission. Les performances en calcul flottant sont assez impressionnantes pour un processeur de l'époque. [[File:ZEN - émission multiple flottante.png|centre|vignette|upright=2|Microarchitecture Zen 1 d'AMD.]] ==Les ports d'émission et de décodage== Les ports d'émission et de décodage sont des ressources matérielles comme les autres. Dans cette section, nous allons parler de concepts liés aux ports d'émission et de décodage. Nous allons notamment parler du partage des ports d'émission, ainsi que des relations numériques entre ces deux types de ports. ===Le partage des ports d'émission et la ''port contention''=== Pour le moment, nous n'avons vu que des cas où un port d'émission est relié soit à des ALU entières, soit à des FPU, pas aux deux. Mais il existe des processeurs où un même port d'émission alimente à la fois une ALU entière et une FPU. Par exemple, on peut relier un additionneur flottant sur le même port qu'une ALU entière. Il faut noter que cela implique une fenêtre d'instruction centralisée, capable de mettre en attente micro-opérations entières et flottantes. Un exemple est celui des processeurs Core 2 Duo. Ils disposent de 6 ports d'émission, dont 3 ports dédiés à l'unité mémoire. Les 3 ports restants alimentent chacun : une ALU entière, un circuit de calcul flottant et une unité de calcul SSE (une unité de calcul SIMD, qu'on abordera dans quelques chapitres). * Le premier port alimente une ALU entière simple et un multiplieur/diviseur flottant. * Le second alimente une ALU entière, un multiplieur entier et un additionneur flottant. * Le troisième alimente une ALU entière, sans circuit flottant dédié. Dans le même genre, il est possible de partager un port d'émission entre l'unité mémoire et une ALU entière. Cela permet d'utiliser l'ALU entière pour les calculs d'adresse, ce qui évite d'avoir à utiliser une unité de calcul d'adresse distincte. Un exemple est celui du processeur superscalaire double émission Power PC 440. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. L'organisation en question permet soit d'émettre un accès mémoire en même temps qu'une opération entière, soit d'émettre deux opérations entières simples, soit d’émettre une multiplication et une addition/soustraction/comparaison. Une organisation simple, mais très efficace ! [[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]] ===La séparation entre largeur de décodage et d'émission=== Sur les processeurs superscalaires larges, le processeur peut émettre plus d'instructions qu'il peut en décoder. Plus haut, j'ai dit que l'unité d'émission avait parfois plus de ports d'émission que de ports de décodage. La raison est assez simple, et elle s'explique assez bien avec un exemple. Prenons un processeur avec trois ALU entières, une FPU, une unité de branchement et une unité mémoire. Chacune a son propre port d'émission, ce qui fait que le CPU est capable d'émettre 6 µops. Du moins, c'est la théorie, car les conditions pour sont assez rares. Il faut qu'en chargeant un paquet de 6 instructions, il y ait exactement trois instructions entières, une flottante, un branchement et un accès mémoire. Si le paquet contient 5 instructions entières et un branchement, seules quatre instructions sont émises. Dans cet exemple, dans la grande majorité des cas, seuls 4 ports seront utilisés en moyenne et deux seront sous-utilisés. Il s'agit là d'une moyenne, qui dépend du code, et des instructions exactes à exécuter. Cependant, cette moyenne nous donne un moyen d'économiser beaucoup de circuits pour un cout en performance mineur. L'idée est de ne charger que des blocs de 4 instructions, mais d'avoir 6 ports d'émission. Le processeur charge et décode 4 instructions, les quatre instructions sont envoyées sur les ports d'émission adéquat. Le cout en transistors est alors situé entre celui d'un CPU quadruple émission et sextuple émission. Et les performances sont aussi entre les deux. Pour les performances, c'est parce que cela permet d'avoir un CPU quadruple émission avec bien moins de contraintes d’appariement. Imaginez qu'on se soit limité à quatre ports d'émission : il aurait fallu mettre plusieurs unités sur le même port d'émission. En conséquence, il y aurait eu des combinaisons interdites, comme émettre deux instructions entières + un branchement + un accès mémoire ; ou deux instructions entières + une opération flottante + un accès mémoire. De tells appariement interdits sont possibles en utilisant les six ports d'émission. ==L'émission multiple des accès mémoire== Les premiers processeurs superscalaires ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles. Par contre, c'est autre chose sur les processeurs superscalaires larges. Vu qu'ils travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares. L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. La ''load/store queue'' reste une structure unique, mais on peut lui rajouter des ports. Et à ce peit jeu, il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire. [[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]] 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. Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre. Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Le Pentium 4 était dans ce cas, mais il était un peu seul dans sa catégorie. Un cas similaire est celui où l'unité mémoire est alimentée par une fenêtre de micro-opération mélangeant instructions mémoire et entières. Les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. Dans ce cas, l'émission multiple est rarement implémentée. Le cas suivant a une file pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique. Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui pouvaient seulement émettre une lecture en même temps qu'une écriture. Ils avaient deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse. [[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]] Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture demande d'utiliser une file d'écriture multiport, ce qui est assez compliqué. Implémenter des lectures en plus est plus simple : il suffit de rajouter une seconde file de µops LOAD et une unité de calcul, alimentées via un second port d'émission. Il est aussi possible de rajouter un second port de lecture au cache, mais ce n'est pas obligatoire. Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse. De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports de lecture et d'écriture car il y a plus de lecture que d'écritures. Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. : Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse. [[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]] Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettent plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail. Pour résumer, l'évolution des CPU superscalaires a été la suivante : * pipeline mémorie fusionné avec le pipeline entier ; * séparation du pipeline mémoire du reste, avec émission parallèle des accès mémoire ; * émission multiple, mais sans dupliquer les ports du cache ; * émission multiple, avec émission simultanée d'une lecture et d’une écriture dans le cache ; * émission multiple avec plusieurs accès en lecture au cache. ==Les optimisations des processeurs superscalaires larges== Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul. ===L'étape de chargement d'un CPU superscalaire large=== Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 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 autoaligné.]] Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements ! [[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]] Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez 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 microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total). Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois 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 microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement. Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations. ===La micro-fusion des processeurs Intel et AMD=== La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''. Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''. Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout. La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Le parallélisme mémoire | prevText=Le parallélisme mémoire | next=Exemples de microarchitectures CPU : le cas du x86 | nextText=Exemples de microarchitectures CPU : le cas du x86 }} </noinclude> 1erfcwqkt8iegv05pxco77cfew194iy 772716 772715 2026-09-21T18:37:47Z Mewtow 31375 /* L'émission multiple des accès mémoire */ 772716 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 tout premiers processeurs superscalaires pouvaient émettre deux µops en même temps, ce qui portait le nom de '''double émission'''. Mais le nombre a rapidement augmenté et les processeurs modernes 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. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps. 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. 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. Deux branchements consécutifs sont quant à eux plus fréquents, mais exécuter deux branchements en même temps pose un problème : que faire quand les deux sont pris ? Deux accès mémoire consécutifs est assez rare. La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs sont systématiquement supportées, car dupliquer des ALU 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 et 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'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. Par exemple, l'unité de chargement charge deux fois plus d'instructions, mais n'est pas modifiée en profondeur. L'unité d'émission et les circuits d'exécution dans le désordre sont eux aussi altérés. Les unités de calcul sont souvent dupliquées, sauf sur les tout premiers CPU superscalaires. {|class="wikitable" |- ! rowspan="2" | Processeur sans émission multiple | rowspan="2" | Chargement | rowspan="2" | Décodage | rowspan="2" | Renommage | rowspan="2" | Émission | Exécution / ALU | rowspan="2" | ''Commit''/ROB |- | Exécution / ALU |- ! rowspan="4" | Processeur superscalaire | rowspan="4" | Chargement | rowspan="2" | Décodage | rowspan="4" | Renommage | rowspan="4" | Émission | Exécution / ALU | rowspan="4" | ''Commit''/ROB |- | Exécution / ALU |- | rowspan="2" | Décodage | Exécution / ALU |- | Exécution / ALU |} Voyons ce qu'il en est dans le détail. ===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. 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. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant. 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. ===Le chargement de plusieurs instructions consécutives=== Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large. Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence. Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas. Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement non-pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc. [[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]] ===Le décodage parallèle des instructions=== Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits. Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur. Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! ===L'unité d'émission d'un processeur superscalaire=== La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. 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 4 instructions, compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément deux 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. 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. : 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 microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle. 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=== Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement. 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. ==L'évolution historique des processeurs superscalaires== Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs triple émission permettent eux d’émettre trois instructions à la fois. Les tout premiers processeurs superscalaires avaient un budget en transistors très limité, ce qui fait qu'ils ne pouvaient pas être "totalement superscalaires". Par exemple, un processeur double émission ne pouvait pas émettre deux opérations entières en même temps, ni deux opérations flottantes, ni une opération entière avec un accès mémoire. Il y avait des paires d'instructions autorisées et des paires interdites. Les '''contraintes d'appariement''' sur les instructions à émettre en même temps étaient très strictes. L'intérêt de ces contraintes d’appariement est que le processeur était très simple, il n'y avait pas besoin de rajouter des circuits complexes pour rendre le processeur superscalaire. Les CPU superscalaires historiques ne faisaient que mieux utiliser des unités de calcul déjà présentes. Prenons un processeur non-superscalaire simple, illustré ci-dessous. Il contient : une unité de chargement, un décodeur, une unité d'émission, une ALU, une FPU, les bancs de registre entier et flottants, et une unité mémoire. Nous avons placé l'unité mémoire à la suite de l'ALU entière, car ce sera important pour les explications qui vont suivre. [[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]] Les processeurs superscalaires historiques modifiaient assez peu le schéma précédent. L'unité de chargement était adaptée pour lire plusieurs instructions depuis la mémoire, il y avait un seul décodeur, l'unité d'émission était assez peu modifiée. Mais une chose était certaine : aucune unité de calcul n'était dupliquée, le banc de registre n'était presque pas modifié. A la rigueur, gérer la triple 'émission avec les branchements demandait parfois d'ajouter des ports de lecture sur le banc de registre généraux, pas plus. ===La double émission entière-flottante=== Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est d'émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Le processeur a donc deux pipelines séparés : un pour les micro-opérations entières, un autre pour les micro-opérations flottantes. On parle alors de '''double émission entière-flottante'''. {|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, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité. La plupart des premiers processeurs superscalaires étaient de ce type. Les exemples les plus notables sont les processeurs POWER 1 et ses dérivés comme le ''RISC Single Chip''. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle. [[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]] La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisations visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate]. ===L'émission parallèle des branchements=== Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur. 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 modifier le banc de registres. Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. [[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]] Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications et les divisions ? Pour étudier ces questions, nous allons prendre le cas d'un processeur capable d’émettre trois opérations en même temps : deux opérations entières, et une flottante, potentiellement d'autres. {|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 |} 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|Emission multiple des opérations entières, implémentation courante]] Pour les branchements, deux solutions sont possibles. Avec la première, il y a émission parallèle des branchements, à savoir qu'il est possible d'émettre en branchement en parallèle de deux-trois instructions entières. Et cela impose la présence d'une unité de branchement séparée, avec son port d'émission. Avec la seconde, les branchements sont traités comme des instructions entières. Soit les branchements sont exécutés dans une ALU entière, soit il y a une unité de branchement reliée à un port d'émission pour les opérations entières. [[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Emission série et parallèle des branchements]] Avec la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales, sur les processeurs superscalaires historiques. En clair, le processeur peut émettre 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. De plus, 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. Ce compromis change cependant quand on passe à la quadruple émission. {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 |- | * ALU entière * Multiplieur | * ALU entière * Barrel shifter * Unité de branchement * Unité mémoire | FPU |- | * Additions, soustractions, opérations bit à bit, etc. * Multiplication | * Additions, soustractions, opérations bit à bit, etc. * Décalages et rotations * Branchements * Accès mémoire |Opération flottante |} ===Les CPU superscalaires à plusieurs unités spécialisées=== Pour la triple émission, deux opérations entières + une flottante est l'idéal. Même les processeurs modernes à triple émission restent sur cette "norme". Mais arrivé à la quadruple émission, les CPU superscalaires ont évolués dans des directions différentes, chacun faisant à sa sauce. La raison est que la quadruple émission permet de nombreuses possibilités, mais la plupart ne sont pas intéressantes. Prenons un CPU superscalaire avec une ALU entière, une FPU, une unité de branchement, et une unité mémoire. Il est possible de créer un CPU quadruple émission avec ces unités, mais le gain en performance n'en vaudrait pas la peine. Il est rare qu'un bloc de 4 instructions contienne exactement une opération entière, une flottante, un branchement et un accès mémoire. En pratique, les seules combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. En conséquence, une fois arrivé à la quadruple émission, les processeurs superscalaires n'ont pas le choix que de rajouter des unités de calcul, voire des unités mémoire. Et ils ont privilégié l'ajout d'ALU entières. La raison est que les programmes informatiques utilisent beaucoup plus d'additions/soustractions que le reste. Il y a environ 5 opérations entières de base pour une multiplication entière, le ratio est le même quand on remplace la multiplication par un branchement ou un accès mémoire. En conséquence, les ALU entières ont été dupliquées en premier, et dans de nombreux exemplaires. Reste à voir comment a été dupliqué le reste. Les multiplieurs n'ont pas été dupliqués, sauf sur les processeurs superscalaires très larges. La raison est qu'il est rare de devoir exécuter deux multiplications à la fois. Et c'est la même chose pour le ''barrel shifter''. Deux branchements consécutifs sont quant à eux plus fréquents, c'est même assez courant. Aussi, les unités de branchement ont été dupliquées en second lieu, après les ALU entières. Avoir plusieurs accès mémoire proches est assez courant, surtout sur les CPU de type CISC. Aussi, les unités mémoire ont été dupliquées en troisième lieu. Pour résumer, dupliquer des ALU entières est donc la priorité, suivi par la duplication des unités de branchement, puis celle des accès mémoire, pour terminer par le multiplieur et le ''barrel shifter''. La FPU est un peu à part. La '''double émission flottante''' a été utilisée sur les processeurs Alpha. L'idée est d'émettre deux opérations flottantes à la fois. Sauf qu'une implémentation naïve demanderait de dupliquer la FPU, ce qui aurait un cout en circuit rédhibitoire. Pour rappel, les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant. L'idée est d'avoir des ports d'émission séparés pour l'additionneur flottant et le multiplieur flottant. Le processeur peut alors émettre une addition flottante en même temps qu'une multiplication flottante. Les autres circuits de calcul flottant sont répartis sur ces deux ports d'émission flottants. L'avantage est que cela se marie bien avec l'usage d'une fenêtre d'instruction séparée pour les opérations flottantes. La fenêtre d'instruction a alors deux ports séparés, au lieu d'un seul. Rajouter un second port d'émission flottant n'est pas trop un problème, car le cout lié à l'ajout d'un port n'est pas linéaire. Passer de un port à deux a un cout tolérable, bien plus que de passer de 3 ports à 4 ou de 4 à 5. Il est possible de dupliquer l'additionneur et le multiplieur flottant, ce qui permet d'émettre deux additions et multiplications flottantes en même temps. C'est ce qui est fait sur les processeurs AMD d'architecture Zen 1 et 2. Ils ont deux additionneurs flottants par cœur, deux multiplieurs flottants, chacun avec leur propre port d'émission. Les performances en calcul flottant sont assez impressionnantes pour un processeur de l'époque. [[File:ZEN - émission multiple flottante.png|centre|vignette|upright=2|Microarchitecture Zen 1 d'AMD.]] ==Les ports d'émission et de décodage== Les ports d'émission et de décodage sont des ressources matérielles comme les autres. Dans cette section, nous allons parler de concepts liés aux ports d'émission et de décodage. Nous allons notamment parler du partage des ports d'émission, ainsi que des relations numériques entre ces deux types de ports. ===Le partage des ports d'émission et la ''port contention''=== Pour le moment, nous n'avons vu que des cas où un port d'émission est relié soit à des ALU entières, soit à des FPU, pas aux deux. Mais il existe des processeurs où un même port d'émission alimente à la fois une ALU entière et une FPU. Par exemple, on peut relier un additionneur flottant sur le même port qu'une ALU entière. Il faut noter que cela implique une fenêtre d'instruction centralisée, capable de mettre en attente micro-opérations entières et flottantes. Un exemple est celui des processeurs Core 2 Duo. Ils disposent de 6 ports d'émission, dont 3 ports dédiés à l'unité mémoire. Les 3 ports restants alimentent chacun : une ALU entière, un circuit de calcul flottant et une unité de calcul SSE (une unité de calcul SIMD, qu'on abordera dans quelques chapitres). * Le premier port alimente une ALU entière simple et un multiplieur/diviseur flottant. * Le second alimente une ALU entière, un multiplieur entier et un additionneur flottant. * Le troisième alimente une ALU entière, sans circuit flottant dédié. Dans le même genre, il est possible de partager un port d'émission entre l'unité mémoire et une ALU entière. Cela permet d'utiliser l'ALU entière pour les calculs d'adresse, ce qui évite d'avoir à utiliser une unité de calcul d'adresse distincte. Un exemple est celui du processeur superscalaire double émission Power PC 440. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. L'organisation en question permet soit d'émettre un accès mémoire en même temps qu'une opération entière, soit d'émettre deux opérations entières simples, soit d’émettre une multiplication et une addition/soustraction/comparaison. Une organisation simple, mais très efficace ! [[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]] ===La séparation entre largeur de décodage et d'émission=== Sur les processeurs superscalaires larges, le processeur peut émettre plus d'instructions qu'il peut en décoder. Plus haut, j'ai dit que l'unité d'émission avait parfois plus de ports d'émission que de ports de décodage. La raison est assez simple, et elle s'explique assez bien avec un exemple. Prenons un processeur avec trois ALU entières, une FPU, une unité de branchement et une unité mémoire. Chacune a son propre port d'émission, ce qui fait que le CPU est capable d'émettre 6 µops. Du moins, c'est la théorie, car les conditions pour sont assez rares. Il faut qu'en chargeant un paquet de 6 instructions, il y ait exactement trois instructions entières, une flottante, un branchement et un accès mémoire. Si le paquet contient 5 instructions entières et un branchement, seules quatre instructions sont émises. Dans cet exemple, dans la grande majorité des cas, seuls 4 ports seront utilisés en moyenne et deux seront sous-utilisés. Il s'agit là d'une moyenne, qui dépend du code, et des instructions exactes à exécuter. Cependant, cette moyenne nous donne un moyen d'économiser beaucoup de circuits pour un cout en performance mineur. L'idée est de ne charger que des blocs de 4 instructions, mais d'avoir 6 ports d'émission. Le processeur charge et décode 4 instructions, les quatre instructions sont envoyées sur les ports d'émission adéquat. Le cout en transistors est alors situé entre celui d'un CPU quadruple émission et sextuple émission. Et les performances sont aussi entre les deux. Pour les performances, c'est parce que cela permet d'avoir un CPU quadruple émission avec bien moins de contraintes d’appariement. Imaginez qu'on se soit limité à quatre ports d'émission : il aurait fallu mettre plusieurs unités sur le même port d'émission. En conséquence, il y aurait eu des combinaisons interdites, comme émettre deux instructions entières + un branchement + un accès mémoire ; ou deux instructions entières + une opération flottante + un accès mémoire. De tells appariement interdits sont possibles en utilisant les six ports d'émission. ==L'émission parallèle des accès mémoire== 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 simultanné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. De plus, 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. Ce compromis change cependant quand on passe à la quadruple émission. {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 |- | * ALU entière * Multiplieur | * ALU entière * Barrel shifter * Unité de branchement * Unité mémoire | FPU |- | * Additions, soustractions, opérations bit à bit, etc. * Multiplication | * Additions, soustractions, opérations bit à bit, etc. * Décalages et rotations * Branchements * Accès mémoire |Opération flottante |} ===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 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. Par contre, c'est autre chose sur les processeurs superscalaires larges. Vu qu'ils travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares. L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. La ''load/store queue'' reste une structure unique, mais on peut lui rajouter des ports. Et à ce peit jeu, il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire. [[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]] 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. Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre. Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Le Pentium 4 était dans ce cas, mais il était un peu seul dans sa catégorie. Un cas similaire est celui où l'unité mémoire est alimentée par une fenêtre de micro-opération mélangeant instructions mémoire et entières. Les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. Dans ce cas, l'émission multiple est rarement implémentée. Le cas suivant a une file pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique. Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui pouvaient seulement émettre une lecture en même temps qu'une écriture. Ils avaient deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse. [[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]] Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture demande d'utiliser une file d'écriture multiport, ce qui est assez compliqué. Implémenter des lectures en plus est plus simple : il suffit de rajouter une seconde file de µops LOAD et une unité de calcul, alimentées via un second port d'émission. Il est aussi possible de rajouter un second port de lecture au cache, mais ce n'est pas obligatoire. Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse. De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports de lecture et d'écriture car il y a plus de lecture que d'écritures. Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. : Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse. [[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]] Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettent plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail. ==Les optimisations des processeurs superscalaires larges== Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul. ===L'étape de chargement d'un CPU superscalaire large=== Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 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 autoaligné.]] Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements ! [[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]] Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez 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 microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total). Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois 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 microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement. Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations. ===La micro-fusion des processeurs Intel et AMD=== La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''. Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''. Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout. La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Le parallélisme mémoire | prevText=Le parallélisme mémoire | next=Exemples de microarchitectures CPU : le cas du x86 | nextText=Exemples de microarchitectures CPU : le cas du x86 }} </noinclude> lre0ymdyzagpiqfd2wlv4xedufmszm0 772717 772716 2026-09-21T18:37:53Z Mewtow 31375 /* L'émission multiple des micro-opérations entières */ 772717 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 tout premiers processeurs superscalaires pouvaient émettre deux µops en même temps, ce qui portait le nom de '''double émission'''. Mais le nombre a rapidement augmenté et les processeurs modernes 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. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps. 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. 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. Deux branchements consécutifs sont quant à eux plus fréquents, mais exécuter deux branchements en même temps pose un problème : que faire quand les deux sont pris ? Deux accès mémoire consécutifs est assez rare. La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs sont systématiquement supportées, car dupliquer des ALU 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 et 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'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. Par exemple, l'unité de chargement charge deux fois plus d'instructions, mais n'est pas modifiée en profondeur. L'unité d'émission et les circuits d'exécution dans le désordre sont eux aussi altérés. Les unités de calcul sont souvent dupliquées, sauf sur les tout premiers CPU superscalaires. {|class="wikitable" |- ! rowspan="2" | Processeur sans émission multiple | rowspan="2" | Chargement | rowspan="2" | Décodage | rowspan="2" | Renommage | rowspan="2" | Émission | Exécution / ALU | rowspan="2" | ''Commit''/ROB |- | Exécution / ALU |- ! rowspan="4" | Processeur superscalaire | rowspan="4" | Chargement | rowspan="2" | Décodage | rowspan="4" | Renommage | rowspan="4" | Émission | Exécution / ALU | rowspan="4" | ''Commit''/ROB |- | Exécution / ALU |- | rowspan="2" | Décodage | Exécution / ALU |- | Exécution / ALU |} Voyons ce qu'il en est dans le détail. ===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. 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. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant. 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. ===Le chargement de plusieurs instructions consécutives=== Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large. Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence. Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas. Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement non-pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc. [[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]] ===Le décodage parallèle des instructions=== Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits. Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur. Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! ===L'unité d'émission d'un processeur superscalaire=== La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. 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 4 instructions, compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément deux 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. 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. : 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 microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle. 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=== Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement. 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. ==L'évolution historique des processeurs superscalaires== Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs triple émission permettent eux d’émettre trois instructions à la fois. Les tout premiers processeurs superscalaires avaient un budget en transistors très limité, ce qui fait qu'ils ne pouvaient pas être "totalement superscalaires". Par exemple, un processeur double émission ne pouvait pas émettre deux opérations entières en même temps, ni deux opérations flottantes, ni une opération entière avec un accès mémoire. Il y avait des paires d'instructions autorisées et des paires interdites. Les '''contraintes d'appariement''' sur les instructions à émettre en même temps étaient très strictes. L'intérêt de ces contraintes d’appariement est que le processeur était très simple, il n'y avait pas besoin de rajouter des circuits complexes pour rendre le processeur superscalaire. Les CPU superscalaires historiques ne faisaient que mieux utiliser des unités de calcul déjà présentes. Prenons un processeur non-superscalaire simple, illustré ci-dessous. Il contient : une unité de chargement, un décodeur, une unité d'émission, une ALU, une FPU, les bancs de registre entier et flottants, et une unité mémoire. Nous avons placé l'unité mémoire à la suite de l'ALU entière, car ce sera important pour les explications qui vont suivre. [[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]] Les processeurs superscalaires historiques modifiaient assez peu le schéma précédent. L'unité de chargement était adaptée pour lire plusieurs instructions depuis la mémoire, il y avait un seul décodeur, l'unité d'émission était assez peu modifiée. Mais une chose était certaine : aucune unité de calcul n'était dupliquée, le banc de registre n'était presque pas modifié. A la rigueur, gérer la triple 'émission avec les branchements demandait parfois d'ajouter des ports de lecture sur le banc de registre généraux, pas plus. ===La double émission entière-flottante=== Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est d'émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Le processeur a donc deux pipelines séparés : un pour les micro-opérations entières, un autre pour les micro-opérations flottantes. On parle alors de '''double émission entière-flottante'''. {|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, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité. La plupart des premiers processeurs superscalaires étaient de ce type. Les exemples les plus notables sont les processeurs POWER 1 et ses dérivés comme le ''RISC Single Chip''. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle. [[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]] La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisations visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate]. ===L'émission parallèle des branchements=== Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur. 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 modifier le banc de registres. Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. [[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]] Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications et les divisions ? Pour étudier ces questions, nous allons prendre le cas d'un processeur capable d’émettre trois opérations en même temps : deux opérations entières, et une flottante, potentiellement d'autres. {|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 |} 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|Emission multiple des opérations entières, implémentation courante]] Pour les branchements, deux solutions sont possibles. Avec la première, il y a émission parallèle des branchements, à savoir qu'il est possible d'émettre en branchement en parallèle de deux-trois instructions entières. Et cela impose la présence d'une unité de branchement séparée, avec son port d'émission. Avec la seconde, les branchements sont traités comme des instructions entières. Soit les branchements sont exécutés dans une ALU entière, soit il y a une unité de branchement reliée à un port d'émission pour les opérations entières. [[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Emission série et parallèle des branchements]] ===Les CPU superscalaires à plusieurs unités spécialisées=== Pour la triple émission, deux opérations entières + une flottante est l'idéal. Même les processeurs modernes à triple émission restent sur cette "norme". Mais arrivé à la quadruple émission, les CPU superscalaires ont évolués dans des directions différentes, chacun faisant à sa sauce. La raison est que la quadruple émission permet de nombreuses possibilités, mais la plupart ne sont pas intéressantes. Prenons un CPU superscalaire avec une ALU entière, une FPU, une unité de branchement, et une unité mémoire. Il est possible de créer un CPU quadruple émission avec ces unités, mais le gain en performance n'en vaudrait pas la peine. Il est rare qu'un bloc de 4 instructions contienne exactement une opération entière, une flottante, un branchement et un accès mémoire. En pratique, les seules combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. En conséquence, une fois arrivé à la quadruple émission, les processeurs superscalaires n'ont pas le choix que de rajouter des unités de calcul, voire des unités mémoire. Et ils ont privilégié l'ajout d'ALU entières. La raison est que les programmes informatiques utilisent beaucoup plus d'additions/soustractions que le reste. Il y a environ 5 opérations entières de base pour une multiplication entière, le ratio est le même quand on remplace la multiplication par un branchement ou un accès mémoire. En conséquence, les ALU entières ont été dupliquées en premier, et dans de nombreux exemplaires. Reste à voir comment a été dupliqué le reste. Les multiplieurs n'ont pas été dupliqués, sauf sur les processeurs superscalaires très larges. La raison est qu'il est rare de devoir exécuter deux multiplications à la fois. Et c'est la même chose pour le ''barrel shifter''. Deux branchements consécutifs sont quant à eux plus fréquents, c'est même assez courant. Aussi, les unités de branchement ont été dupliquées en second lieu, après les ALU entières. Avoir plusieurs accès mémoire proches est assez courant, surtout sur les CPU de type CISC. Aussi, les unités mémoire ont été dupliquées en troisième lieu. Pour résumer, dupliquer des ALU entières est donc la priorité, suivi par la duplication des unités de branchement, puis celle des accès mémoire, pour terminer par le multiplieur et le ''barrel shifter''. La FPU est un peu à part. La '''double émission flottante''' a été utilisée sur les processeurs Alpha. L'idée est d'émettre deux opérations flottantes à la fois. Sauf qu'une implémentation naïve demanderait de dupliquer la FPU, ce qui aurait un cout en circuit rédhibitoire. Pour rappel, les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant. L'idée est d'avoir des ports d'émission séparés pour l'additionneur flottant et le multiplieur flottant. Le processeur peut alors émettre une addition flottante en même temps qu'une multiplication flottante. Les autres circuits de calcul flottant sont répartis sur ces deux ports d'émission flottants. L'avantage est que cela se marie bien avec l'usage d'une fenêtre d'instruction séparée pour les opérations flottantes. La fenêtre d'instruction a alors deux ports séparés, au lieu d'un seul. Rajouter un second port d'émission flottant n'est pas trop un problème, car le cout lié à l'ajout d'un port n'est pas linéaire. Passer de un port à deux a un cout tolérable, bien plus que de passer de 3 ports à 4 ou de 4 à 5. Il est possible de dupliquer l'additionneur et le multiplieur flottant, ce qui permet d'émettre deux additions et multiplications flottantes en même temps. C'est ce qui est fait sur les processeurs AMD d'architecture Zen 1 et 2. Ils ont deux additionneurs flottants par cœur, deux multiplieurs flottants, chacun avec leur propre port d'émission. Les performances en calcul flottant sont assez impressionnantes pour un processeur de l'époque. [[File:ZEN - émission multiple flottante.png|centre|vignette|upright=2|Microarchitecture Zen 1 d'AMD.]] ==Les ports d'émission et de décodage== Les ports d'émission et de décodage sont des ressources matérielles comme les autres. Dans cette section, nous allons parler de concepts liés aux ports d'émission et de décodage. Nous allons notamment parler du partage des ports d'émission, ainsi que des relations numériques entre ces deux types de ports. ===Le partage des ports d'émission et la ''port contention''=== Pour le moment, nous n'avons vu que des cas où un port d'émission est relié soit à des ALU entières, soit à des FPU, pas aux deux. Mais il existe des processeurs où un même port d'émission alimente à la fois une ALU entière et une FPU. Par exemple, on peut relier un additionneur flottant sur le même port qu'une ALU entière. Il faut noter que cela implique une fenêtre d'instruction centralisée, capable de mettre en attente micro-opérations entières et flottantes. Un exemple est celui des processeurs Core 2 Duo. Ils disposent de 6 ports d'émission, dont 3 ports dédiés à l'unité mémoire. Les 3 ports restants alimentent chacun : une ALU entière, un circuit de calcul flottant et une unité de calcul SSE (une unité de calcul SIMD, qu'on abordera dans quelques chapitres). * Le premier port alimente une ALU entière simple et un multiplieur/diviseur flottant. * Le second alimente une ALU entière, un multiplieur entier et un additionneur flottant. * Le troisième alimente une ALU entière, sans circuit flottant dédié. Dans le même genre, il est possible de partager un port d'émission entre l'unité mémoire et une ALU entière. Cela permet d'utiliser l'ALU entière pour les calculs d'adresse, ce qui évite d'avoir à utiliser une unité de calcul d'adresse distincte. Un exemple est celui du processeur superscalaire double émission Power PC 440. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. L'organisation en question permet soit d'émettre un accès mémoire en même temps qu'une opération entière, soit d'émettre deux opérations entières simples, soit d’émettre une multiplication et une addition/soustraction/comparaison. Une organisation simple, mais très efficace ! [[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]] ===La séparation entre largeur de décodage et d'émission=== Sur les processeurs superscalaires larges, le processeur peut émettre plus d'instructions qu'il peut en décoder. Plus haut, j'ai dit que l'unité d'émission avait parfois plus de ports d'émission que de ports de décodage. La raison est assez simple, et elle s'explique assez bien avec un exemple. Prenons un processeur avec trois ALU entières, une FPU, une unité de branchement et une unité mémoire. Chacune a son propre port d'émission, ce qui fait que le CPU est capable d'émettre 6 µops. Du moins, c'est la théorie, car les conditions pour sont assez rares. Il faut qu'en chargeant un paquet de 6 instructions, il y ait exactement trois instructions entières, une flottante, un branchement et un accès mémoire. Si le paquet contient 5 instructions entières et un branchement, seules quatre instructions sont émises. Dans cet exemple, dans la grande majorité des cas, seuls 4 ports seront utilisés en moyenne et deux seront sous-utilisés. Il s'agit là d'une moyenne, qui dépend du code, et des instructions exactes à exécuter. Cependant, cette moyenne nous donne un moyen d'économiser beaucoup de circuits pour un cout en performance mineur. L'idée est de ne charger que des blocs de 4 instructions, mais d'avoir 6 ports d'émission. Le processeur charge et décode 4 instructions, les quatre instructions sont envoyées sur les ports d'émission adéquat. Le cout en transistors est alors situé entre celui d'un CPU quadruple émission et sextuple émission. Et les performances sont aussi entre les deux. Pour les performances, c'est parce que cela permet d'avoir un CPU quadruple émission avec bien moins de contraintes d’appariement. Imaginez qu'on se soit limité à quatre ports d'émission : il aurait fallu mettre plusieurs unités sur le même port d'émission. En conséquence, il y aurait eu des combinaisons interdites, comme émettre deux instructions entières + un branchement + un accès mémoire ; ou deux instructions entières + une opération flottante + un accès mémoire. De tells appariement interdits sont possibles en utilisant les six ports d'émission. ==L'émission parallèle des accès mémoire== 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 simultanné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. De plus, 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. Ce compromis change cependant quand on passe à la quadruple émission. {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 |- | * ALU entière * Multiplieur | * ALU entière * Barrel shifter * Unité de branchement * Unité mémoire | FPU |- | * Additions, soustractions, opérations bit à bit, etc. * Multiplication | * Additions, soustractions, opérations bit à bit, etc. * Décalages et rotations * Branchements * Accès mémoire |Opération flottante |} ===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 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. Par contre, c'est autre chose sur les processeurs superscalaires larges. Vu qu'ils travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares. L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. La ''load/store queue'' reste une structure unique, mais on peut lui rajouter des ports. Et à ce peit jeu, il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire. [[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]] 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. Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre. Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Le Pentium 4 était dans ce cas, mais il était un peu seul dans sa catégorie. Un cas similaire est celui où l'unité mémoire est alimentée par une fenêtre de micro-opération mélangeant instructions mémoire et entières. Les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. Dans ce cas, l'émission multiple est rarement implémentée. Le cas suivant a une file pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique. Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui pouvaient seulement émettre une lecture en même temps qu'une écriture. Ils avaient deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse. [[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]] Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture demande d'utiliser une file d'écriture multiport, ce qui est assez compliqué. Implémenter des lectures en plus est plus simple : il suffit de rajouter une seconde file de µops LOAD et une unité de calcul, alimentées via un second port d'émission. Il est aussi possible de rajouter un second port de lecture au cache, mais ce n'est pas obligatoire. Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse. De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports de lecture et d'écriture car il y a plus de lecture que d'écritures. Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. : Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse. [[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]] Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettent plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail. ==Les optimisations des processeurs superscalaires larges== Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul. ===L'étape de chargement d'un CPU superscalaire large=== Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 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 autoaligné.]] Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements ! [[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]] Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez 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 microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total). Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois 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 microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement. Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations. ===La micro-fusion des processeurs Intel et AMD=== La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''. Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''. Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout. La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Le parallélisme mémoire | prevText=Le parallélisme mémoire | next=Exemples de microarchitectures CPU : le cas du x86 | nextText=Exemples de microarchitectures CPU : le cas du x86 }} </noinclude> 6y2u10p33gsy02egfu0zdxgl4lvsgej 772718 772717 2026-09-21T18:40:12Z Mewtow 31375 /* L'évolution historique des processeurs superscalaires */ 772718 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 tout premiers processeurs superscalaires pouvaient émettre deux µops en même temps, ce qui portait le nom de '''double émission'''. Mais le nombre a rapidement augmenté et les processeurs modernes 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. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps. 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. 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. Deux branchements consécutifs sont quant à eux plus fréquents, mais exécuter deux branchements en même temps pose un problème : que faire quand les deux sont pris ? Deux accès mémoire consécutifs est assez rare. La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs sont systématiquement supportées, car dupliquer des ALU 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 et 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'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. Par exemple, l'unité de chargement charge deux fois plus d'instructions, mais n'est pas modifiée en profondeur. L'unité d'émission et les circuits d'exécution dans le désordre sont eux aussi altérés. Les unités de calcul sont souvent dupliquées, sauf sur les tout premiers CPU superscalaires. {|class="wikitable" |- ! rowspan="2" | Processeur sans émission multiple | rowspan="2" | Chargement | rowspan="2" | Décodage | rowspan="2" | Renommage | rowspan="2" | Émission | Exécution / ALU | rowspan="2" | ''Commit''/ROB |- | Exécution / ALU |- ! rowspan="4" | Processeur superscalaire | rowspan="4" | Chargement | rowspan="2" | Décodage | rowspan="4" | Renommage | rowspan="4" | Émission | Exécution / ALU | rowspan="4" | ''Commit''/ROB |- | Exécution / ALU |- | rowspan="2" | Décodage | Exécution / ALU |- | Exécution / ALU |} Voyons ce qu'il en est dans le détail. ===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. 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. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant. 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. ===Le chargement de plusieurs instructions consécutives=== Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large. Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence. Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas. Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement non-pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc. [[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]] ===Le décodage parallèle des instructions=== Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits. Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur. Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! ===L'unité d'émission d'un processeur superscalaire=== La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. 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 4 instructions, compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément deux 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. 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. : 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 microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle. 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=== Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement. 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. ==L'évolution historique des processeurs superscalaires== Les tout premiers processeurs superscalaires avaient un budget en transistors très limité, ce qui fait que les contraintes d'appariement étaient très strictes. Les CPU superscalaires historiques ne faisaient que mieux utiliser des unités de calcul déjà présentes. Prenons un processeur non-superscalaire simple, illustré ci-dessous. Il contient : une unité de chargement, un décodeur, une unité d'émission, une ALU, une FPU, les bancs de registre entier et flottants, et une unité mémoire. Nous avons placé l'unité mémoire à la suite de l'ALU entière, car ce sera important pour les explications qui vont suivre. [[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]] Les processeurs superscalaires historiques modifiaient assez peu le schéma précédent. L'unité de chargement était adaptée pour lire plusieurs instructions depuis la mémoire, il y avait un seul décodeur, l'unité d'émission était assez peu modifiée. Mais une chose était certaine : aucune unité de calcul n'était dupliquée, le banc de registre n'était presque pas modifié. A la rigueur, gérer la triple 'émission avec les branchements demandait parfois d'ajouter des ports de lecture sur le banc de registre généraux, pas plus. ===La double émission entière-flottante=== Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est d'émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Le processeur a donc deux pipelines séparés : un pour les micro-opérations entières, un autre pour les micro-opérations flottantes. On parle alors de '''double émission entière-flottante'''. {|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, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité. La plupart des premiers processeurs superscalaires étaient de ce type. Les exemples les plus notables sont les processeurs POWER 1 et ses dérivés comme le ''RISC Single Chip''. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle. [[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]] La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisations visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate]. ===L'émission parallèle des branchements=== Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur. 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 modifier le banc de registres. Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. [[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]] Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications et les divisions ? Pour étudier ces questions, nous allons prendre le cas d'un processeur capable d’émettre trois opérations en même temps : deux opérations entières, et une flottante, potentiellement d'autres. {|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 |} 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|Emission multiple des opérations entières, implémentation courante]] Pour les branchements, deux solutions sont possibles. Avec la première, il y a émission parallèle des branchements, à savoir qu'il est possible d'émettre en branchement en parallèle de deux-trois instructions entières. Et cela impose la présence d'une unité de branchement séparée, avec son port d'émission. Avec la seconde, les branchements sont traités comme des instructions entières. Soit les branchements sont exécutés dans une ALU entière, soit il y a une unité de branchement reliée à un port d'émission pour les opérations entières. [[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Emission série et parallèle des branchements]] ===Les CPU superscalaires à plusieurs unités spécialisées=== Pour la triple émission, deux opérations entières + une flottante est l'idéal. Même les processeurs modernes à triple émission restent sur cette "norme". Mais arrivé à la quadruple émission, les CPU superscalaires ont évolués dans des directions différentes, chacun faisant à sa sauce. La raison est que la quadruple émission permet de nombreuses possibilités, mais la plupart ne sont pas intéressantes. Prenons un CPU superscalaire avec une ALU entière, une FPU, une unité de branchement, et une unité mémoire. Il est possible de créer un CPU quadruple émission avec ces unités, mais le gain en performance n'en vaudrait pas la peine. Il est rare qu'un bloc de 4 instructions contienne exactement une opération entière, une flottante, un branchement et un accès mémoire. En pratique, les seules combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. En conséquence, une fois arrivé à la quadruple émission, les processeurs superscalaires n'ont pas le choix que de rajouter des unités de calcul, voire des unités mémoire. Et ils ont privilégié l'ajout d'ALU entières. La raison est que les programmes informatiques utilisent beaucoup plus d'additions/soustractions que le reste. Il y a environ 5 opérations entières de base pour une multiplication entière, le ratio est le même quand on remplace la multiplication par un branchement ou un accès mémoire. En conséquence, les ALU entières ont été dupliquées en premier, et dans de nombreux exemplaires. Reste à voir comment a été dupliqué le reste. Les multiplieurs n'ont pas été dupliqués, sauf sur les processeurs superscalaires très larges. La raison est qu'il est rare de devoir exécuter deux multiplications à la fois. Et c'est la même chose pour le ''barrel shifter''. Deux branchements consécutifs sont quant à eux plus fréquents, c'est même assez courant. Aussi, les unités de branchement ont été dupliquées en second lieu, après les ALU entières. Avoir plusieurs accès mémoire proches est assez courant, surtout sur les CPU de type CISC. Aussi, les unités mémoire ont été dupliquées en troisième lieu. Pour résumer, dupliquer des ALU entières est donc la priorité, suivi par la duplication des unités de branchement, puis celle des accès mémoire, pour terminer par le multiplieur et le ''barrel shifter''. La FPU est un peu à part. La '''double émission flottante''' a été utilisée sur les processeurs Alpha. L'idée est d'émettre deux opérations flottantes à la fois. Sauf qu'une implémentation naïve demanderait de dupliquer la FPU, ce qui aurait un cout en circuit rédhibitoire. Pour rappel, les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant. L'idée est d'avoir des ports d'émission séparés pour l'additionneur flottant et le multiplieur flottant. Le processeur peut alors émettre une addition flottante en même temps qu'une multiplication flottante. Les autres circuits de calcul flottant sont répartis sur ces deux ports d'émission flottants. L'avantage est que cela se marie bien avec l'usage d'une fenêtre d'instruction séparée pour les opérations flottantes. La fenêtre d'instruction a alors deux ports séparés, au lieu d'un seul. Rajouter un second port d'émission flottant n'est pas trop un problème, car le cout lié à l'ajout d'un port n'est pas linéaire. Passer de un port à deux a un cout tolérable, bien plus que de passer de 3 ports à 4 ou de 4 à 5. Il est possible de dupliquer l'additionneur et le multiplieur flottant, ce qui permet d'émettre deux additions et multiplications flottantes en même temps. C'est ce qui est fait sur les processeurs AMD d'architecture Zen 1 et 2. Ils ont deux additionneurs flottants par cœur, deux multiplieurs flottants, chacun avec leur propre port d'émission. Les performances en calcul flottant sont assez impressionnantes pour un processeur de l'époque. [[File:ZEN - émission multiple flottante.png|centre|vignette|upright=2|Microarchitecture Zen 1 d'AMD.]] ==Les ports d'émission et de décodage== Les ports d'émission et de décodage sont des ressources matérielles comme les autres. Dans cette section, nous allons parler de concepts liés aux ports d'émission et de décodage. Nous allons notamment parler du partage des ports d'émission, ainsi que des relations numériques entre ces deux types de ports. ===Le partage des ports d'émission et la ''port contention''=== Pour le moment, nous n'avons vu que des cas où un port d'émission est relié soit à des ALU entières, soit à des FPU, pas aux deux. Mais il existe des processeurs où un même port d'émission alimente à la fois une ALU entière et une FPU. Par exemple, on peut relier un additionneur flottant sur le même port qu'une ALU entière. Il faut noter que cela implique une fenêtre d'instruction centralisée, capable de mettre en attente micro-opérations entières et flottantes. Un exemple est celui des processeurs Core 2 Duo. Ils disposent de 6 ports d'émission, dont 3 ports dédiés à l'unité mémoire. Les 3 ports restants alimentent chacun : une ALU entière, un circuit de calcul flottant et une unité de calcul SSE (une unité de calcul SIMD, qu'on abordera dans quelques chapitres). * Le premier port alimente une ALU entière simple et un multiplieur/diviseur flottant. * Le second alimente une ALU entière, un multiplieur entier et un additionneur flottant. * Le troisième alimente une ALU entière, sans circuit flottant dédié. Dans le même genre, il est possible de partager un port d'émission entre l'unité mémoire et une ALU entière. Cela permet d'utiliser l'ALU entière pour les calculs d'adresse, ce qui évite d'avoir à utiliser une unité de calcul d'adresse distincte. Un exemple est celui du processeur superscalaire double émission Power PC 440. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. L'organisation en question permet soit d'émettre un accès mémoire en même temps qu'une opération entière, soit d'émettre deux opérations entières simples, soit d’émettre une multiplication et une addition/soustraction/comparaison. Une organisation simple, mais très efficace ! [[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]] ===La séparation entre largeur de décodage et d'émission=== Sur les processeurs superscalaires larges, le processeur peut émettre plus d'instructions qu'il peut en décoder. Plus haut, j'ai dit que l'unité d'émission avait parfois plus de ports d'émission que de ports de décodage. La raison est assez simple, et elle s'explique assez bien avec un exemple. Prenons un processeur avec trois ALU entières, une FPU, une unité de branchement et une unité mémoire. Chacune a son propre port d'émission, ce qui fait que le CPU est capable d'émettre 6 µops. Du moins, c'est la théorie, car les conditions pour sont assez rares. Il faut qu'en chargeant un paquet de 6 instructions, il y ait exactement trois instructions entières, une flottante, un branchement et un accès mémoire. Si le paquet contient 5 instructions entières et un branchement, seules quatre instructions sont émises. Dans cet exemple, dans la grande majorité des cas, seuls 4 ports seront utilisés en moyenne et deux seront sous-utilisés. Il s'agit là d'une moyenne, qui dépend du code, et des instructions exactes à exécuter. Cependant, cette moyenne nous donne un moyen d'économiser beaucoup de circuits pour un cout en performance mineur. L'idée est de ne charger que des blocs de 4 instructions, mais d'avoir 6 ports d'émission. Le processeur charge et décode 4 instructions, les quatre instructions sont envoyées sur les ports d'émission adéquat. Le cout en transistors est alors situé entre celui d'un CPU quadruple émission et sextuple émission. Et les performances sont aussi entre les deux. Pour les performances, c'est parce que cela permet d'avoir un CPU quadruple émission avec bien moins de contraintes d’appariement. Imaginez qu'on se soit limité à quatre ports d'émission : il aurait fallu mettre plusieurs unités sur le même port d'émission. En conséquence, il y aurait eu des combinaisons interdites, comme émettre deux instructions entières + un branchement + un accès mémoire ; ou deux instructions entières + une opération flottante + un accès mémoire. De tells appariement interdits sont possibles en utilisant les six ports d'émission. ==L'émission parallèle des accès mémoire== 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 simultanné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. De plus, 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. Ce compromis change cependant quand on passe à la quadruple émission. {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 |- | * ALU entière * Multiplieur | * ALU entière * Barrel shifter * Unité de branchement * Unité mémoire | FPU |- | * Additions, soustractions, opérations bit à bit, etc. * Multiplication | * Additions, soustractions, opérations bit à bit, etc. * Décalages et rotations * Branchements * Accès mémoire |Opération flottante |} ===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 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. Par contre, c'est autre chose sur les processeurs superscalaires larges. Vu qu'ils travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares. L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. La ''load/store queue'' reste une structure unique, mais on peut lui rajouter des ports. Et à ce peit jeu, il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire. [[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]] 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. Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre. Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Le Pentium 4 était dans ce cas, mais il était un peu seul dans sa catégorie. Un cas similaire est celui où l'unité mémoire est alimentée par une fenêtre de micro-opération mélangeant instructions mémoire et entières. Les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. Dans ce cas, l'émission multiple est rarement implémentée. Le cas suivant a une file pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique. Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui pouvaient seulement émettre une lecture en même temps qu'une écriture. Ils avaient deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse. [[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]] Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture demande d'utiliser une file d'écriture multiport, ce qui est assez compliqué. Implémenter des lectures en plus est plus simple : il suffit de rajouter une seconde file de µops LOAD et une unité de calcul, alimentées via un second port d'émission. Il est aussi possible de rajouter un second port de lecture au cache, mais ce n'est pas obligatoire. Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse. De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports de lecture et d'écriture car il y a plus de lecture que d'écritures. Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. : Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse. [[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]] Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettent plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail. ==Les optimisations des processeurs superscalaires larges== Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul. ===L'étape de chargement d'un CPU superscalaire large=== Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 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 autoaligné.]] Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements ! [[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]] Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez 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 microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total). Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois 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 microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement. Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations. ===La micro-fusion des processeurs Intel et AMD=== La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''. Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''. Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout. La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Le parallélisme mémoire | prevText=Le parallélisme mémoire | next=Exemples de microarchitectures CPU : le cas du x86 | nextText=Exemples de microarchitectures CPU : le cas du x86 }} </noinclude> 9ovm6mqht4dp8k1zc3u9ppxnwio7kbb 772719 772718 2026-09-21T18:40:53Z Mewtow 31375 772719 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. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps. 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. 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. Deux branchements consécutifs sont quant à eux plus fréquents, mais exécuter deux branchements en même temps pose un problème : que faire quand les deux sont pris ? Deux accès mémoire consécutifs est assez rare. La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs sont systématiquement supportées, car dupliquer des ALU 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 et 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'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. Par exemple, l'unité de chargement charge deux fois plus d'instructions, mais n'est pas modifiée en profondeur. L'unité d'émission et les circuits d'exécution dans le désordre sont eux aussi altérés. Les unités de calcul sont souvent dupliquées, sauf sur les tout premiers CPU superscalaires. {|class="wikitable" |- ! rowspan="2" | Processeur sans émission multiple | rowspan="2" | Chargement | rowspan="2" | Décodage | rowspan="2" | Renommage | rowspan="2" | Émission | Exécution / ALU | rowspan="2" | ''Commit''/ROB |- | Exécution / ALU |- ! rowspan="4" | Processeur superscalaire | rowspan="4" | Chargement | rowspan="2" | Décodage | rowspan="4" | Renommage | rowspan="4" | Émission | Exécution / ALU | rowspan="4" | ''Commit''/ROB |- | Exécution / ALU |- | rowspan="2" | Décodage | Exécution / ALU |- | Exécution / ALU |} Voyons ce qu'il en est dans le détail. ===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. 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. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant. 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. ===Le chargement de plusieurs instructions consécutives=== Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large. Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence. Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas. Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement non-pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc. [[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]] ===Le décodage parallèle des instructions=== Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits. Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur. Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! ===L'unité d'émission d'un processeur superscalaire=== La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. 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 4 instructions, compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément deux 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. 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. : 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 microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle. 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=== Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement. 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. ==L'évolution historique des processeurs superscalaires== Les tout premiers processeurs superscalaires avaient un budget en transistors très limité, ce qui fait que les contraintes d'appariement étaient très strictes. Les CPU superscalaires historiques ne faisaient que mieux utiliser des unités de calcul déjà présentes. Prenons un processeur non-superscalaire simple, illustré ci-dessous. Il contient : une unité de chargement, un décodeur, une unité d'émission, une ALU, une FPU, les bancs de registre entier et flottants, et une unité mémoire. Nous avons placé l'unité mémoire à la suite de l'ALU entière, car ce sera important pour les explications qui vont suivre. [[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]] Les processeurs superscalaires historiques modifiaient assez peu le schéma précédent. L'unité de chargement était adaptée pour lire plusieurs instructions depuis la mémoire, il y avait un seul décodeur, l'unité d'émission était assez peu modifiée. Mais une chose était certaine : aucune unité de calcul n'était dupliquée, le banc de registre n'était presque pas modifié. A la rigueur, gérer la triple 'émission avec les branchements demandait parfois d'ajouter des ports de lecture sur le banc de registre généraux, pas plus. ===La double émission entière-flottante=== Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est d'émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Le processeur a donc deux pipelines séparés : un pour les micro-opérations entières, un autre pour les micro-opérations flottantes. On parle alors de '''double émission entière-flottante'''. {|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, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité. La plupart des premiers processeurs superscalaires étaient de ce type. Les exemples les plus notables sont les processeurs POWER 1 et ses dérivés comme le ''RISC Single Chip''. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle. [[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]] La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisations visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate]. ===L'émission parallèle des branchements=== Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur. 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 modifier le banc de registres. Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. [[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]] Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications et les divisions ? Pour étudier ces questions, nous allons prendre le cas d'un processeur capable d’émettre trois opérations en même temps : deux opérations entières, et une flottante, potentiellement d'autres. {|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 |} 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|Emission multiple des opérations entières, implémentation courante]] Pour les branchements, deux solutions sont possibles. Avec la première, il y a émission parallèle des branchements, à savoir qu'il est possible d'émettre en branchement en parallèle de deux-trois instructions entières. Et cela impose la présence d'une unité de branchement séparée, avec son port d'émission. Avec la seconde, les branchements sont traités comme des instructions entières. Soit les branchements sont exécutés dans une ALU entière, soit il y a une unité de branchement reliée à un port d'émission pour les opérations entières. [[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Emission série et parallèle des branchements]] ===Les CPU superscalaires à plusieurs unités spécialisées=== Pour la triple émission, deux opérations entières + une flottante est l'idéal. Même les processeurs modernes à triple émission restent sur cette "norme". Mais arrivé à la quadruple émission, les CPU superscalaires ont évolués dans des directions différentes, chacun faisant à sa sauce. La raison est que la quadruple émission permet de nombreuses possibilités, mais la plupart ne sont pas intéressantes. Prenons un CPU superscalaire avec une ALU entière, une FPU, une unité de branchement, et une unité mémoire. Il est possible de créer un CPU quadruple émission avec ces unités, mais le gain en performance n'en vaudrait pas la peine. Il est rare qu'un bloc de 4 instructions contienne exactement une opération entière, une flottante, un branchement et un accès mémoire. En pratique, les seules combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. En conséquence, une fois arrivé à la quadruple émission, les processeurs superscalaires n'ont pas le choix que de rajouter des unités de calcul, voire des unités mémoire. Et ils ont privilégié l'ajout d'ALU entières. La raison est que les programmes informatiques utilisent beaucoup plus d'additions/soustractions que le reste. Il y a environ 5 opérations entières de base pour une multiplication entière, le ratio est le même quand on remplace la multiplication par un branchement ou un accès mémoire. En conséquence, les ALU entières ont été dupliquées en premier, et dans de nombreux exemplaires. Reste à voir comment a été dupliqué le reste. Les multiplieurs n'ont pas été dupliqués, sauf sur les processeurs superscalaires très larges. La raison est qu'il est rare de devoir exécuter deux multiplications à la fois. Et c'est la même chose pour le ''barrel shifter''. Deux branchements consécutifs sont quant à eux plus fréquents, c'est même assez courant. Aussi, les unités de branchement ont été dupliquées en second lieu, après les ALU entières. Avoir plusieurs accès mémoire proches est assez courant, surtout sur les CPU de type CISC. Aussi, les unités mémoire ont été dupliquées en troisième lieu. Pour résumer, dupliquer des ALU entières est donc la priorité, suivi par la duplication des unités de branchement, puis celle des accès mémoire, pour terminer par le multiplieur et le ''barrel shifter''. La FPU est un peu à part. La '''double émission flottante''' a été utilisée sur les processeurs Alpha. L'idée est d'émettre deux opérations flottantes à la fois. Sauf qu'une implémentation naïve demanderait de dupliquer la FPU, ce qui aurait un cout en circuit rédhibitoire. Pour rappel, les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant. L'idée est d'avoir des ports d'émission séparés pour l'additionneur flottant et le multiplieur flottant. Le processeur peut alors émettre une addition flottante en même temps qu'une multiplication flottante. Les autres circuits de calcul flottant sont répartis sur ces deux ports d'émission flottants. L'avantage est que cela se marie bien avec l'usage d'une fenêtre d'instruction séparée pour les opérations flottantes. La fenêtre d'instruction a alors deux ports séparés, au lieu d'un seul. Rajouter un second port d'émission flottant n'est pas trop un problème, car le cout lié à l'ajout d'un port n'est pas linéaire. Passer de un port à deux a un cout tolérable, bien plus que de passer de 3 ports à 4 ou de 4 à 5. Il est possible de dupliquer l'additionneur et le multiplieur flottant, ce qui permet d'émettre deux additions et multiplications flottantes en même temps. C'est ce qui est fait sur les processeurs AMD d'architecture Zen 1 et 2. Ils ont deux additionneurs flottants par cœur, deux multiplieurs flottants, chacun avec leur propre port d'émission. Les performances en calcul flottant sont assez impressionnantes pour un processeur de l'époque. [[File:ZEN - émission multiple flottante.png|centre|vignette|upright=2|Microarchitecture Zen 1 d'AMD.]] ==Les ports d'émission et de décodage== Les ports d'émission et de décodage sont des ressources matérielles comme les autres. Dans cette section, nous allons parler de concepts liés aux ports d'émission et de décodage. Nous allons notamment parler du partage des ports d'émission, ainsi que des relations numériques entre ces deux types de ports. ===Le partage des ports d'émission et la ''port contention''=== Pour le moment, nous n'avons vu que des cas où un port d'émission est relié soit à des ALU entières, soit à des FPU, pas aux deux. Mais il existe des processeurs où un même port d'émission alimente à la fois une ALU entière et une FPU. Par exemple, on peut relier un additionneur flottant sur le même port qu'une ALU entière. Il faut noter que cela implique une fenêtre d'instruction centralisée, capable de mettre en attente micro-opérations entières et flottantes. Un exemple est celui des processeurs Core 2 Duo. Ils disposent de 6 ports d'émission, dont 3 ports dédiés à l'unité mémoire. Les 3 ports restants alimentent chacun : une ALU entière, un circuit de calcul flottant et une unité de calcul SSE (une unité de calcul SIMD, qu'on abordera dans quelques chapitres). * Le premier port alimente une ALU entière simple et un multiplieur/diviseur flottant. * Le second alimente une ALU entière, un multiplieur entier et un additionneur flottant. * Le troisième alimente une ALU entière, sans circuit flottant dédié. Dans le même genre, il est possible de partager un port d'émission entre l'unité mémoire et une ALU entière. Cela permet d'utiliser l'ALU entière pour les calculs d'adresse, ce qui évite d'avoir à utiliser une unité de calcul d'adresse distincte. Un exemple est celui du processeur superscalaire double émission Power PC 440. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. L'organisation en question permet soit d'émettre un accès mémoire en même temps qu'une opération entière, soit d'émettre deux opérations entières simples, soit d’émettre une multiplication et une addition/soustraction/comparaison. Une organisation simple, mais très efficace ! [[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]] ===La séparation entre largeur de décodage et d'émission=== Sur les processeurs superscalaires larges, le processeur peut émettre plus d'instructions qu'il peut en décoder. Plus haut, j'ai dit que l'unité d'émission avait parfois plus de ports d'émission que de ports de décodage. La raison est assez simple, et elle s'explique assez bien avec un exemple. Prenons un processeur avec trois ALU entières, une FPU, une unité de branchement et une unité mémoire. Chacune a son propre port d'émission, ce qui fait que le CPU est capable d'émettre 6 µops. Du moins, c'est la théorie, car les conditions pour sont assez rares. Il faut qu'en chargeant un paquet de 6 instructions, il y ait exactement trois instructions entières, une flottante, un branchement et un accès mémoire. Si le paquet contient 5 instructions entières et un branchement, seules quatre instructions sont émises. Dans cet exemple, dans la grande majorité des cas, seuls 4 ports seront utilisés en moyenne et deux seront sous-utilisés. Il s'agit là d'une moyenne, qui dépend du code, et des instructions exactes à exécuter. Cependant, cette moyenne nous donne un moyen d'économiser beaucoup de circuits pour un cout en performance mineur. L'idée est de ne charger que des blocs de 4 instructions, mais d'avoir 6 ports d'émission. Le processeur charge et décode 4 instructions, les quatre instructions sont envoyées sur les ports d'émission adéquat. Le cout en transistors est alors situé entre celui d'un CPU quadruple émission et sextuple émission. Et les performances sont aussi entre les deux. Pour les performances, c'est parce que cela permet d'avoir un CPU quadruple émission avec bien moins de contraintes d’appariement. Imaginez qu'on se soit limité à quatre ports d'émission : il aurait fallu mettre plusieurs unités sur le même port d'émission. En conséquence, il y aurait eu des combinaisons interdites, comme émettre deux instructions entières + un branchement + un accès mémoire ; ou deux instructions entières + une opération flottante + un accès mémoire. De tells appariement interdits sont possibles en utilisant les six ports d'émission. ==L'émission parallèle des accès mémoire== 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 simultanné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. De plus, 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. Ce compromis change cependant quand on passe à la quadruple émission. {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 |- | * ALU entière * Multiplieur | * ALU entière * Barrel shifter * Unité de branchement * Unité mémoire | FPU |- | * Additions, soustractions, opérations bit à bit, etc. * Multiplication | * Additions, soustractions, opérations bit à bit, etc. * Décalages et rotations * Branchements * Accès mémoire |Opération flottante |} ===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 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. Par contre, c'est autre chose sur les processeurs superscalaires larges. Vu qu'ils travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares. L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. La ''load/store queue'' reste une structure unique, mais on peut lui rajouter des ports. Et à ce peit jeu, il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire. [[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]] 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. Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre. Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Le Pentium 4 était dans ce cas, mais il était un peu seul dans sa catégorie. Un cas similaire est celui où l'unité mémoire est alimentée par une fenêtre de micro-opération mélangeant instructions mémoire et entières. Les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. Dans ce cas, l'émission multiple est rarement implémentée. Le cas suivant a une file pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique. Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui pouvaient seulement émettre une lecture en même temps qu'une écriture. Ils avaient deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse. [[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]] Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture demande d'utiliser une file d'écriture multiport, ce qui est assez compliqué. Implémenter des lectures en plus est plus simple : il suffit de rajouter une seconde file de µops LOAD et une unité de calcul, alimentées via un second port d'émission. Il est aussi possible de rajouter un second port de lecture au cache, mais ce n'est pas obligatoire. Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse. De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports de lecture et d'écriture car il y a plus de lecture que d'écritures. Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. : Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse. [[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]] Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettent plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail. ==Les optimisations des processeurs superscalaires larges== Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul. ===L'étape de chargement d'un CPU superscalaire large=== Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 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 autoaligné.]] Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements ! [[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]] Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez 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 microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total). Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois 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 microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement. Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations. ===La micro-fusion des processeurs Intel et AMD=== La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''. Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''. Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout. La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Le parallélisme mémoire | prevText=Le parallélisme mémoire | next=Exemples de microarchitectures CPU : le cas du x86 | nextText=Exemples de microarchitectures CPU : le cas du x86 }} </noinclude> bn63h1ch03um4r38tazhr39bizo28gk 772720 772719 2026-09-21T18:41:37Z Mewtow 31375 /* L'évolution historique des processeurs superscalaires */ 772720 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. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps. 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. 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. Deux branchements consécutifs sont quant à eux plus fréquents, mais exécuter deux branchements en même temps pose un problème : que faire quand les deux sont pris ? Deux accès mémoire consécutifs est assez rare. La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs sont systématiquement supportées, car dupliquer des ALU 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 et 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'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. Par exemple, l'unité de chargement charge deux fois plus d'instructions, mais n'est pas modifiée en profondeur. L'unité d'émission et les circuits d'exécution dans le désordre sont eux aussi altérés. Les unités de calcul sont souvent dupliquées, sauf sur les tout premiers CPU superscalaires. {|class="wikitable" |- ! rowspan="2" | Processeur sans émission multiple | rowspan="2" | Chargement | rowspan="2" | Décodage | rowspan="2" | Renommage | rowspan="2" | Émission | Exécution / ALU | rowspan="2" | ''Commit''/ROB |- | Exécution / ALU |- ! rowspan="4" | Processeur superscalaire | rowspan="4" | Chargement | rowspan="2" | Décodage | rowspan="4" | Renommage | rowspan="4" | Émission | Exécution / ALU | rowspan="4" | ''Commit''/ROB |- | Exécution / ALU |- | rowspan="2" | Décodage | Exécution / ALU |- | Exécution / ALU |} Voyons ce qu'il en est dans le détail. ===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. 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. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant. 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. ===Le chargement de plusieurs instructions consécutives=== Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large. Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence. Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas. Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement non-pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc. [[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]] ===Le décodage parallèle des instructions=== Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits. Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur. Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! ===L'unité d'émission d'un processeur superscalaire=== La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. 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 4 instructions, compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément deux 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. 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. : 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 microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle. 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=== Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement. 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. ==L'évolution historique des processeurs superscalaires== Les tout premiers processeurs superscalaires avaient un budget en transistors très limité, ce qui fait que les contraintes d'appariement étaient très strictes. Et au-delà de ça, ils ne faisaient que mieux utiliser des unités de calcul déjà présentes, ils ne dupliquaient aucune unité de calcul. Prenons un processeur non-superscalaire simple, illustré ci-dessous. Il contient : une unité de chargement, un décodeur, une unité d'émission, une ALU, une FPU, les bancs de registre entier et flottants, et une unité mémoire. Nous avons placé l'unité mémoire à la suite de l'ALU entière, car ce sera important pour les explications qui vont suivre. [[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]] Les processeurs superscalaires historiques modifiaient assez peu le schéma précédent. L'unité de chargement était adaptée pour lire plusieurs instructions depuis la mémoire, il y avait un seul décodeur, l'unité d'émission était assez peu modifiée. Mais une chose était certaine : aucune unité de calcul n'était dupliquée, le banc de registre n'était presque pas modifié. A la rigueur, gérer la triple 'émission avec les branchements demandait parfois d'ajouter des ports de lecture sur le banc de registre généraux, pas plus. ===La double émission entière-flottante=== Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est d'émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Le processeur a donc deux pipelines séparés : un pour les micro-opérations entières, un autre pour les micro-opérations flottantes. On parle alors de '''double émission entière-flottante'''. {|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, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité. La plupart des premiers processeurs superscalaires étaient de ce type. Les exemples les plus notables sont les processeurs POWER 1 et ses dérivés comme le ''RISC Single Chip''. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle. [[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]] La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisations visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate]. ===L'émission parallèle des branchements=== Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur. 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 modifier le banc de registres. Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. [[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]] Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications et les divisions ? Pour étudier ces questions, nous allons prendre le cas d'un processeur capable d’émettre trois opérations en même temps : deux opérations entières, et une flottante, potentiellement d'autres. {|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 |} 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|Emission multiple des opérations entières, implémentation courante]] Pour les branchements, deux solutions sont possibles. Avec la première, il y a émission parallèle des branchements, à savoir qu'il est possible d'émettre en branchement en parallèle de deux-trois instructions entières. Et cela impose la présence d'une unité de branchement séparée, avec son port d'émission. Avec la seconde, les branchements sont traités comme des instructions entières. Soit les branchements sont exécutés dans une ALU entière, soit il y a une unité de branchement reliée à un port d'émission pour les opérations entières. [[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Emission série et parallèle des branchements]] ===Les CPU superscalaires à plusieurs unités spécialisées=== Pour la triple émission, deux opérations entières + une flottante est l'idéal. Même les processeurs modernes à triple émission restent sur cette "norme". Mais arrivé à la quadruple émission, les CPU superscalaires ont évolués dans des directions différentes, chacun faisant à sa sauce. La raison est que la quadruple émission permet de nombreuses possibilités, mais la plupart ne sont pas intéressantes. Prenons un CPU superscalaire avec une ALU entière, une FPU, une unité de branchement, et une unité mémoire. Il est possible de créer un CPU quadruple émission avec ces unités, mais le gain en performance n'en vaudrait pas la peine. Il est rare qu'un bloc de 4 instructions contienne exactement une opération entière, une flottante, un branchement et un accès mémoire. En pratique, les seules combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. En conséquence, une fois arrivé à la quadruple émission, les processeurs superscalaires n'ont pas le choix que de rajouter des unités de calcul, voire des unités mémoire. Et ils ont privilégié l'ajout d'ALU entières. La raison est que les programmes informatiques utilisent beaucoup plus d'additions/soustractions que le reste. Il y a environ 5 opérations entières de base pour une multiplication entière, le ratio est le même quand on remplace la multiplication par un branchement ou un accès mémoire. En conséquence, les ALU entières ont été dupliquées en premier, et dans de nombreux exemplaires. Reste à voir comment a été dupliqué le reste. Les multiplieurs n'ont pas été dupliqués, sauf sur les processeurs superscalaires très larges. La raison est qu'il est rare de devoir exécuter deux multiplications à la fois. Et c'est la même chose pour le ''barrel shifter''. Deux branchements consécutifs sont quant à eux plus fréquents, c'est même assez courant. Aussi, les unités de branchement ont été dupliquées en second lieu, après les ALU entières. Avoir plusieurs accès mémoire proches est assez courant, surtout sur les CPU de type CISC. Aussi, les unités mémoire ont été dupliquées en troisième lieu. Pour résumer, dupliquer des ALU entières est donc la priorité, suivi par la duplication des unités de branchement, puis celle des accès mémoire, pour terminer par le multiplieur et le ''barrel shifter''. La FPU est un peu à part. La '''double émission flottante''' a été utilisée sur les processeurs Alpha. L'idée est d'émettre deux opérations flottantes à la fois. Sauf qu'une implémentation naïve demanderait de dupliquer la FPU, ce qui aurait un cout en circuit rédhibitoire. Pour rappel, les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant. L'idée est d'avoir des ports d'émission séparés pour l'additionneur flottant et le multiplieur flottant. Le processeur peut alors émettre une addition flottante en même temps qu'une multiplication flottante. Les autres circuits de calcul flottant sont répartis sur ces deux ports d'émission flottants. L'avantage est que cela se marie bien avec l'usage d'une fenêtre d'instruction séparée pour les opérations flottantes. La fenêtre d'instruction a alors deux ports séparés, au lieu d'un seul. Rajouter un second port d'émission flottant n'est pas trop un problème, car le cout lié à l'ajout d'un port n'est pas linéaire. Passer de un port à deux a un cout tolérable, bien plus que de passer de 3 ports à 4 ou de 4 à 5. Il est possible de dupliquer l'additionneur et le multiplieur flottant, ce qui permet d'émettre deux additions et multiplications flottantes en même temps. C'est ce qui est fait sur les processeurs AMD d'architecture Zen 1 et 2. Ils ont deux additionneurs flottants par cœur, deux multiplieurs flottants, chacun avec leur propre port d'émission. Les performances en calcul flottant sont assez impressionnantes pour un processeur de l'époque. [[File:ZEN - émission multiple flottante.png|centre|vignette|upright=2|Microarchitecture Zen 1 d'AMD.]] ==Les ports d'émission et de décodage== Les ports d'émission et de décodage sont des ressources matérielles comme les autres. Dans cette section, nous allons parler de concepts liés aux ports d'émission et de décodage. Nous allons notamment parler du partage des ports d'émission, ainsi que des relations numériques entre ces deux types de ports. ===Le partage des ports d'émission et la ''port contention''=== Pour le moment, nous n'avons vu que des cas où un port d'émission est relié soit à des ALU entières, soit à des FPU, pas aux deux. Mais il existe des processeurs où un même port d'émission alimente à la fois une ALU entière et une FPU. Par exemple, on peut relier un additionneur flottant sur le même port qu'une ALU entière. Il faut noter que cela implique une fenêtre d'instruction centralisée, capable de mettre en attente micro-opérations entières et flottantes. Un exemple est celui des processeurs Core 2 Duo. Ils disposent de 6 ports d'émission, dont 3 ports dédiés à l'unité mémoire. Les 3 ports restants alimentent chacun : une ALU entière, un circuit de calcul flottant et une unité de calcul SSE (une unité de calcul SIMD, qu'on abordera dans quelques chapitres). * Le premier port alimente une ALU entière simple et un multiplieur/diviseur flottant. * Le second alimente une ALU entière, un multiplieur entier et un additionneur flottant. * Le troisième alimente une ALU entière, sans circuit flottant dédié. Dans le même genre, il est possible de partager un port d'émission entre l'unité mémoire et une ALU entière. Cela permet d'utiliser l'ALU entière pour les calculs d'adresse, ce qui évite d'avoir à utiliser une unité de calcul d'adresse distincte. Un exemple est celui du processeur superscalaire double émission Power PC 440. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. L'organisation en question permet soit d'émettre un accès mémoire en même temps qu'une opération entière, soit d'émettre deux opérations entières simples, soit d’émettre une multiplication et une addition/soustraction/comparaison. Une organisation simple, mais très efficace ! [[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]] ===La séparation entre largeur de décodage et d'émission=== Sur les processeurs superscalaires larges, le processeur peut émettre plus d'instructions qu'il peut en décoder. Plus haut, j'ai dit que l'unité d'émission avait parfois plus de ports d'émission que de ports de décodage. La raison est assez simple, et elle s'explique assez bien avec un exemple. Prenons un processeur avec trois ALU entières, une FPU, une unité de branchement et une unité mémoire. Chacune a son propre port d'émission, ce qui fait que le CPU est capable d'émettre 6 µops. Du moins, c'est la théorie, car les conditions pour sont assez rares. Il faut qu'en chargeant un paquet de 6 instructions, il y ait exactement trois instructions entières, une flottante, un branchement et un accès mémoire. Si le paquet contient 5 instructions entières et un branchement, seules quatre instructions sont émises. Dans cet exemple, dans la grande majorité des cas, seuls 4 ports seront utilisés en moyenne et deux seront sous-utilisés. Il s'agit là d'une moyenne, qui dépend du code, et des instructions exactes à exécuter. Cependant, cette moyenne nous donne un moyen d'économiser beaucoup de circuits pour un cout en performance mineur. L'idée est de ne charger que des blocs de 4 instructions, mais d'avoir 6 ports d'émission. Le processeur charge et décode 4 instructions, les quatre instructions sont envoyées sur les ports d'émission adéquat. Le cout en transistors est alors situé entre celui d'un CPU quadruple émission et sextuple émission. Et les performances sont aussi entre les deux. Pour les performances, c'est parce que cela permet d'avoir un CPU quadruple émission avec bien moins de contraintes d’appariement. Imaginez qu'on se soit limité à quatre ports d'émission : il aurait fallu mettre plusieurs unités sur le même port d'émission. En conséquence, il y aurait eu des combinaisons interdites, comme émettre deux instructions entières + un branchement + un accès mémoire ; ou deux instructions entières + une opération flottante + un accès mémoire. De tells appariement interdits sont possibles en utilisant les six ports d'émission. ==L'émission parallèle des accès mémoire== 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 simultanné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. De plus, 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. Ce compromis change cependant quand on passe à la quadruple émission. {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 |- | * ALU entière * Multiplieur | * ALU entière * Barrel shifter * Unité de branchement * Unité mémoire | FPU |- | * Additions, soustractions, opérations bit à bit, etc. * Multiplication | * Additions, soustractions, opérations bit à bit, etc. * Décalages et rotations * Branchements * Accès mémoire |Opération flottante |} ===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 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. Par contre, c'est autre chose sur les processeurs superscalaires larges. Vu qu'ils travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares. L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. La ''load/store queue'' reste une structure unique, mais on peut lui rajouter des ports. Et à ce peit jeu, il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire. [[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]] 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. Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre. Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Le Pentium 4 était dans ce cas, mais il était un peu seul dans sa catégorie. Un cas similaire est celui où l'unité mémoire est alimentée par une fenêtre de micro-opération mélangeant instructions mémoire et entières. Les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. Dans ce cas, l'émission multiple est rarement implémentée. Le cas suivant a une file pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique. Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui pouvaient seulement émettre une lecture en même temps qu'une écriture. Ils avaient deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse. [[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]] Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture demande d'utiliser une file d'écriture multiport, ce qui est assez compliqué. Implémenter des lectures en plus est plus simple : il suffit de rajouter une seconde file de µops LOAD et une unité de calcul, alimentées via un second port d'émission. Il est aussi possible de rajouter un second port de lecture au cache, mais ce n'est pas obligatoire. Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse. De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports de lecture et d'écriture car il y a plus de lecture que d'écritures. Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. : Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse. [[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]] Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettent plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail. ==Les optimisations des processeurs superscalaires larges== Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul. ===L'étape de chargement d'un CPU superscalaire large=== Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 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 autoaligné.]] Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements ! [[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]] Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez 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 microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total). Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois 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 microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement. Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations. ===La micro-fusion des processeurs Intel et AMD=== La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''. Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''. Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout. La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Le parallélisme mémoire | prevText=Le parallélisme mémoire | next=Exemples de microarchitectures CPU : le cas du x86 | nextText=Exemples de microarchitectures CPU : le cas du x86 }} </noinclude> gvjo4lh3eis0cyz7n1tymmflobtgptr 772721 772720 2026-09-21T18:48:38Z Mewtow 31375 /* Les CPU superscalaires à plusieurs unités spécialisées */ 772721 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. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps. 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. 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. Deux branchements consécutifs sont quant à eux plus fréquents, mais exécuter deux branchements en même temps pose un problème : que faire quand les deux sont pris ? Deux accès mémoire consécutifs est assez rare. La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs sont systématiquement supportées, car dupliquer des ALU 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 et 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'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. Par exemple, l'unité de chargement charge deux fois plus d'instructions, mais n'est pas modifiée en profondeur. L'unité d'émission et les circuits d'exécution dans le désordre sont eux aussi altérés. Les unités de calcul sont souvent dupliquées, sauf sur les tout premiers CPU superscalaires. {|class="wikitable" |- ! rowspan="2" | Processeur sans émission multiple | rowspan="2" | Chargement | rowspan="2" | Décodage | rowspan="2" | Renommage | rowspan="2" | Émission | Exécution / ALU | rowspan="2" | ''Commit''/ROB |- | Exécution / ALU |- ! rowspan="4" | Processeur superscalaire | rowspan="4" | Chargement | rowspan="2" | Décodage | rowspan="4" | Renommage | rowspan="4" | Émission | Exécution / ALU | rowspan="4" | ''Commit''/ROB |- | Exécution / ALU |- | rowspan="2" | Décodage | Exécution / ALU |- | Exécution / ALU |} Voyons ce qu'il en est dans le détail. ===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. 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. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant. 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. ===Le chargement de plusieurs instructions consécutives=== Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large. Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence. Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas. Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement non-pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc. [[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]] ===Le décodage parallèle des instructions=== Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits. Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur. Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! ===L'unité d'émission d'un processeur superscalaire=== La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. 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 4 instructions, compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément deux 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. 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. : 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 microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle. 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=== Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement. 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. ==L'évolution historique des processeurs superscalaires== Les tout premiers processeurs superscalaires avaient un budget en transistors très limité, ce qui fait que les contraintes d'appariement étaient très strictes. Et au-delà de ça, ils ne faisaient que mieux utiliser des unités de calcul déjà présentes, ils ne dupliquaient aucune unité de calcul. Prenons un processeur non-superscalaire simple, illustré ci-dessous. Il contient : une unité de chargement, un décodeur, une unité d'émission, une ALU, une FPU, les bancs de registre entier et flottants, et une unité mémoire. Nous avons placé l'unité mémoire à la suite de l'ALU entière, car ce sera important pour les explications qui vont suivre. [[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]] Les processeurs superscalaires historiques modifiaient assez peu le schéma précédent. L'unité de chargement était adaptée pour lire plusieurs instructions depuis la mémoire, il y avait un seul décodeur, l'unité d'émission était assez peu modifiée. Mais une chose était certaine : aucune unité de calcul n'était dupliquée, le banc de registre n'était presque pas modifié. A la rigueur, gérer la triple 'émission avec les branchements demandait parfois d'ajouter des ports de lecture sur le banc de registre généraux, pas plus. ===La double émission entière-flottante=== Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est d'émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Le processeur a donc deux pipelines séparés : un pour les micro-opérations entières, un autre pour les micro-opérations flottantes. On parle alors de '''double émission entière-flottante'''. {|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, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité. La plupart des premiers processeurs superscalaires étaient de ce type. Les exemples les plus notables sont les processeurs POWER 1 et ses dérivés comme le ''RISC Single Chip''. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle. [[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]] La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisations visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate]. ===L'émission parallèle des branchements=== Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur. 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 modifier le banc de registres. Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. [[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]] Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications et les divisions ? Pour étudier ces questions, nous allons prendre le cas d'un processeur capable d’émettre trois opérations en même temps : deux opérations entières, et une flottante, potentiellement d'autres. {|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 |} 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|Emission multiple des opérations entières, implémentation courante]] Pour les branchements, deux solutions sont possibles. Avec la première, il y a émission parallèle des branchements, à savoir qu'il est possible d'émettre en branchement en parallèle de deux-trois instructions entières. Et cela impose la présence d'une unité de branchement séparée, avec son port d'émission. Avec la seconde, les branchements sont traités comme des instructions entières. Soit les branchements sont exécutés dans une ALU entière, soit il y a une unité de branchement reliée à un port d'émission pour les opérations entières. [[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Emission série et parallèle des branchements]] ===Les CPU superscalaires à plusieurs unités spécialisées=== L'évolution des CPU superscalaires s'est faite en quatre temps, avec des design de départ simples, qui ont progressivement évolués. La double émission entière-flottante était la première étape, l'exécution parallèle des branchements la seconde, l'usage de plusieurs ALU entière était la troisième. Mais la quatrième étape ne correspond en réalité pas à grand chose de concret, ni à une évolution technique bien précise. Elle correspond en réalité à l'arrivée de processeur superscalaires très variés, qui n'ont pas grand chose en commun. Le premier CPU superscalaires utilisés dans les PC étaient le Pentium. Et c'était un CPU double émission un peu particulier. Il n'implémentait pas la double émission entière-flottante, mais il avait deux ALU entières. En clair, rien à voir avec une sorte d'évolution des designs précédents. Et il n'est pas le seul, la majorité des CPU superscalaire qui ont suivi ont privilégié l'ajout d'ALU entières. La raison est que les programmes informatiques utilisent beaucoup plus d'additions/soustractions que le reste. Il y a environ 5 opérations entières de base pour une multiplication entière, le ratio est le même quand on remplace la multiplication par un branchement ou un accès mémoire. En conséquence, les ALU entières ont été dupliquées en premier, et dans de nombreux exemplaires. Reste à voir comment a été dupliqué le reste. Les multiplieurs n'ont pas été dupliqués, sauf sur les processeurs superscalaires très larges. La raison est qu'il est rare de devoir exécuter deux multiplications à la fois. Et c'est la même chose pour le ''barrel shifter''. Deux branchements consécutifs sont quant à eux plus fréquents, c'est même assez courant. Aussi, les unités de branchement ont été dupliquées en second lieu, après les ALU entières. Avoir plusieurs accès mémoire proches est assez courant, surtout sur les CPU de type CISC. Aussi, les unités mémoire ont été dupliquées en troisième lieu. Pour résumer, dupliquer des ALU entières est donc la priorité, suivi par la duplication des unités de branchement, puis celle des accès mémoire, pour terminer par le multiplieur et le ''barrel shifter''. La FPU est un peu à part. La '''double émission flottante''' a été utilisée sur les processeurs Alpha. L'idée est d'émettre deux opérations flottantes à la fois. Sauf qu'une implémentation naïve demanderait de dupliquer la FPU, ce qui aurait un cout en circuit rédhibitoire. Pour rappel, les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant. L'idée est d'avoir des ports d'émission séparés pour l'additionneur flottant et le multiplieur flottant. Le processeur peut alors émettre une addition flottante en même temps qu'une multiplication flottante. Les autres circuits de calcul flottant sont répartis sur ces deux ports d'émission flottants. L'avantage est que cela se marie bien avec l'usage d'une fenêtre d'instruction séparée pour les opérations flottantes. La fenêtre d'instruction a alors deux ports séparés, au lieu d'un seul. Rajouter un second port d'émission flottant n'est pas trop un problème, car le cout lié à l'ajout d'un port n'est pas linéaire. Passer de un port à deux a un cout tolérable, bien plus que de passer de 3 ports à 4 ou de 4 à 5. Il est possible de dupliquer l'additionneur et le multiplieur flottant, ce qui permet d'émettre deux additions et multiplications flottantes en même temps. C'est ce qui est fait sur les processeurs AMD d'architecture Zen 1 et 2. Ils ont deux additionneurs flottants par cœur, deux multiplieurs flottants, chacun avec leur propre port d'émission. Les performances en calcul flottant sont assez impressionnantes pour un processeur de l'époque. [[File:ZEN - émission multiple flottante.png|centre|vignette|upright=2|Microarchitecture Zen 1 d'AMD.]] ==Les ports d'émission et de décodage== Les ports d'émission et de décodage sont des ressources matérielles comme les autres. Dans cette section, nous allons parler de concepts liés aux ports d'émission et de décodage. Nous allons notamment parler du partage des ports d'émission, ainsi que des relations numériques entre ces deux types de ports. ===Le partage des ports d'émission et la ''port contention''=== Pour le moment, nous n'avons vu que des cas où un port d'émission est relié soit à des ALU entières, soit à des FPU, pas aux deux. Mais il existe des processeurs où un même port d'émission alimente à la fois une ALU entière et une FPU. Par exemple, on peut relier un additionneur flottant sur le même port qu'une ALU entière. Il faut noter que cela implique une fenêtre d'instruction centralisée, capable de mettre en attente micro-opérations entières et flottantes. Un exemple est celui des processeurs Core 2 Duo. Ils disposent de 6 ports d'émission, dont 3 ports dédiés à l'unité mémoire. Les 3 ports restants alimentent chacun : une ALU entière, un circuit de calcul flottant et une unité de calcul SSE (une unité de calcul SIMD, qu'on abordera dans quelques chapitres). * Le premier port alimente une ALU entière simple et un multiplieur/diviseur flottant. * Le second alimente une ALU entière, un multiplieur entier et un additionneur flottant. * Le troisième alimente une ALU entière, sans circuit flottant dédié. Dans le même genre, il est possible de partager un port d'émission entre l'unité mémoire et une ALU entière. Cela permet d'utiliser l'ALU entière pour les calculs d'adresse, ce qui évite d'avoir à utiliser une unité de calcul d'adresse distincte. Un exemple est celui du processeur superscalaire double émission Power PC 440. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. L'organisation en question permet soit d'émettre un accès mémoire en même temps qu'une opération entière, soit d'émettre deux opérations entières simples, soit d’émettre une multiplication et une addition/soustraction/comparaison. Une organisation simple, mais très efficace ! [[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]] ===La séparation entre largeur de décodage et d'émission=== Sur les processeurs superscalaires larges, le processeur peut émettre plus d'instructions qu'il peut en décoder. Plus haut, j'ai dit que l'unité d'émission avait parfois plus de ports d'émission que de ports de décodage. La raison est assez simple, et elle s'explique assez bien avec un exemple. Prenons un processeur avec trois ALU entières, une FPU, une unité de branchement et une unité mémoire. Chacune a son propre port d'émission, ce qui fait que le CPU est capable d'émettre 6 µops. Du moins, c'est la théorie, car les conditions pour sont assez rares. Il faut qu'en chargeant un paquet de 6 instructions, il y ait exactement trois instructions entières, une flottante, un branchement et un accès mémoire. Si le paquet contient 5 instructions entières et un branchement, seules quatre instructions sont émises. Dans cet exemple, dans la grande majorité des cas, seuls 4 ports seront utilisés en moyenne et deux seront sous-utilisés. Il s'agit là d'une moyenne, qui dépend du code, et des instructions exactes à exécuter. Cependant, cette moyenne nous donne un moyen d'économiser beaucoup de circuits pour un cout en performance mineur. L'idée est de ne charger que des blocs de 4 instructions, mais d'avoir 6 ports d'émission. Le processeur charge et décode 4 instructions, les quatre instructions sont envoyées sur les ports d'émission adéquat. Le cout en transistors est alors situé entre celui d'un CPU quadruple émission et sextuple émission. Et les performances sont aussi entre les deux. Pour les performances, c'est parce que cela permet d'avoir un CPU quadruple émission avec bien moins de contraintes d’appariement. Imaginez qu'on se soit limité à quatre ports d'émission : il aurait fallu mettre plusieurs unités sur le même port d'émission. En conséquence, il y aurait eu des combinaisons interdites, comme émettre deux instructions entières + un branchement + un accès mémoire ; ou deux instructions entières + une opération flottante + un accès mémoire. De tells appariement interdits sont possibles en utilisant les six ports d'émission. ==L'émission parallèle des accès mémoire== 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 simultanné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. De plus, 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. Ce compromis change cependant quand on passe à la quadruple émission. {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 |- | * ALU entière * Multiplieur | * ALU entière * Barrel shifter * Unité de branchement * Unité mémoire | FPU |- | * Additions, soustractions, opérations bit à bit, etc. * Multiplication | * Additions, soustractions, opérations bit à bit, etc. * Décalages et rotations * Branchements * Accès mémoire |Opération flottante |} ===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 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. Par contre, c'est autre chose sur les processeurs superscalaires larges. Vu qu'ils travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares. L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. La ''load/store queue'' reste une structure unique, mais on peut lui rajouter des ports. Et à ce peit jeu, il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire. [[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]] 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. Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre. Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Le Pentium 4 était dans ce cas, mais il était un peu seul dans sa catégorie. Un cas similaire est celui où l'unité mémoire est alimentée par une fenêtre de micro-opération mélangeant instructions mémoire et entières. Les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. Dans ce cas, l'émission multiple est rarement implémentée. Le cas suivant a une file pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique. Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui pouvaient seulement émettre une lecture en même temps qu'une écriture. Ils avaient deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse. [[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]] Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture demande d'utiliser une file d'écriture multiport, ce qui est assez compliqué. Implémenter des lectures en plus est plus simple : il suffit de rajouter une seconde file de µops LOAD et une unité de calcul, alimentées via un second port d'émission. Il est aussi possible de rajouter un second port de lecture au cache, mais ce n'est pas obligatoire. Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse. De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports de lecture et d'écriture car il y a plus de lecture que d'écritures. Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. : Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse. [[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]] Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettent plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail. ==Les optimisations des processeurs superscalaires larges== Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul. ===L'étape de chargement d'un CPU superscalaire large=== Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 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 autoaligné.]] Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements ! [[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]] Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez 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 microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total). Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois 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 microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement. Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations. ===La micro-fusion des processeurs Intel et AMD=== La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''. Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''. Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout. La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Le parallélisme mémoire | prevText=Le parallélisme mémoire | next=Exemples de microarchitectures CPU : le cas du x86 | nextText=Exemples de microarchitectures CPU : le cas du x86 }} </noinclude> d6gd6w48syf19lufkhmljtaujy6apts 772722 772721 2026-09-21T18:49:49Z Mewtow 31375 /* Les CPU superscalaires à plusieurs unités spécialisées */ 772722 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. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps. 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. 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. Deux branchements consécutifs sont quant à eux plus fréquents, mais exécuter deux branchements en même temps pose un problème : que faire quand les deux sont pris ? Deux accès mémoire consécutifs est assez rare. La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs sont systématiquement supportées, car dupliquer des ALU 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 et 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'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. Par exemple, l'unité de chargement charge deux fois plus d'instructions, mais n'est pas modifiée en profondeur. L'unité d'émission et les circuits d'exécution dans le désordre sont eux aussi altérés. Les unités de calcul sont souvent dupliquées, sauf sur les tout premiers CPU superscalaires. {|class="wikitable" |- ! rowspan="2" | Processeur sans émission multiple | rowspan="2" | Chargement | rowspan="2" | Décodage | rowspan="2" | Renommage | rowspan="2" | Émission | Exécution / ALU | rowspan="2" | ''Commit''/ROB |- | Exécution / ALU |- ! rowspan="4" | Processeur superscalaire | rowspan="4" | Chargement | rowspan="2" | Décodage | rowspan="4" | Renommage | rowspan="4" | Émission | Exécution / ALU | rowspan="4" | ''Commit''/ROB |- | Exécution / ALU |- | rowspan="2" | Décodage | Exécution / ALU |- | Exécution / ALU |} Voyons ce qu'il en est dans le détail. ===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. 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. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant. 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. ===Le chargement de plusieurs instructions consécutives=== Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large. Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence. Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas. Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement non-pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc. [[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]] ===Le décodage parallèle des instructions=== Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits. Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur. Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! ===L'unité d'émission d'un processeur superscalaire=== La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. 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 4 instructions, compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément deux 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. 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. : 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 microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle. 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=== Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement. 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. ==L'évolution historique des processeurs superscalaires== Les tout premiers processeurs superscalaires avaient un budget en transistors très limité, ce qui fait que les contraintes d'appariement étaient très strictes. Et au-delà de ça, ils ne faisaient que mieux utiliser des unités de calcul déjà présentes, ils ne dupliquaient aucune unité de calcul. Prenons un processeur non-superscalaire simple, illustré ci-dessous. Il contient : une unité de chargement, un décodeur, une unité d'émission, une ALU, une FPU, les bancs de registre entier et flottants, et une unité mémoire. Nous avons placé l'unité mémoire à la suite de l'ALU entière, car ce sera important pour les explications qui vont suivre. [[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]] Les processeurs superscalaires historiques modifiaient assez peu le schéma précédent. L'unité de chargement était adaptée pour lire plusieurs instructions depuis la mémoire, il y avait un seul décodeur, l'unité d'émission était assez peu modifiée. Mais une chose était certaine : aucune unité de calcul n'était dupliquée, le banc de registre n'était presque pas modifié. A la rigueur, gérer la triple 'émission avec les branchements demandait parfois d'ajouter des ports de lecture sur le banc de registre généraux, pas plus. ===La double émission entière-flottante=== Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est d'émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Le processeur a donc deux pipelines séparés : un pour les micro-opérations entières, un autre pour les micro-opérations flottantes. On parle alors de '''double émission entière-flottante'''. {|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, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité. La plupart des premiers processeurs superscalaires étaient de ce type. Les exemples les plus notables sont les processeurs POWER 1 et ses dérivés comme le ''RISC Single Chip''. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle. [[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]] La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisations visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate]. ===L'émission parallèle des branchements=== Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur. 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 modifier le banc de registres. Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. [[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]] Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications et les divisions ? Pour étudier ces questions, nous allons prendre le cas d'un processeur capable d’émettre trois opérations en même temps : deux opérations entières, et une flottante, potentiellement d'autres. {|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 |} 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|Emission multiple des opérations entières, implémentation courante]] Pour les branchements, deux solutions sont possibles. Avec la première, il y a émission parallèle des branchements, à savoir qu'il est possible d'émettre en branchement en parallèle de deux-trois instructions entières. Et cela impose la présence d'une unité de branchement séparée, avec son port d'émission. Avec la seconde, les branchements sont traités comme des instructions entières. Soit les branchements sont exécutés dans une ALU entière, soit il y a une unité de branchement reliée à un port d'émission pour les opérations entières. [[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Emission série et parallèle des branchements]] ===Les CPU superscalaires à plusieurs unités spécialisées=== L'évolution des CPU superscalaires s'est faite en quatre temps, et nous en sommes à la quatrième étape. Et cette quatrième étape ne correspond en réalité pas à grand chose de concret, ni à une évolution technique bien précise. Elle correspond en réalité à l'arrivée de processeur superscalaires très variés, qui n'ont pas grand chose en commun. Le premier CPU superscalaires utilisés dans les PC étaient le Pentium. Et c'était un CPU double émission un peu particulier. Il n'implémentait pas la double émission entière-flottante, mais il avait deux ALU entières. En clair, rien à voir avec une sorte d'évolution des designs précédents. Et il n'est pas le seul, la majorité des CPU superscalaire qui ont suivi ont privilégié l'ajout d'ALU entières. La raison est que les programmes informatiques utilisent beaucoup plus d'additions/soustractions que le reste. Il y a environ 5 opérations entières de base pour une multiplication entière, le ratio est le même quand on remplace la multiplication par un branchement ou un accès mémoire. En conséquence, les ALU entières ont été dupliquées en premier, et dans de nombreux exemplaires. Reste à voir comment a été dupliqué le reste. Les multiplieurs n'ont pas été dupliqués, sauf sur les processeurs superscalaires très larges. La raison est qu'il est rare de devoir exécuter deux multiplications à la fois. Et c'est la même chose pour le ''barrel shifter''. Deux branchements consécutifs sont quant à eux plus fréquents, c'est même assez courant. Aussi, les unités de branchement ont été dupliquées en second lieu, après les ALU entières. Avoir plusieurs accès mémoire proches est assez courant, surtout sur les CPU de type CISC. Aussi, les unités mémoire ont été dupliquées en troisième lieu. Pour résumer, dupliquer des ALU entières est donc la priorité, suivi par la duplication des unités de branchement, puis celle des accès mémoire, pour terminer par le multiplieur et le ''barrel shifter''. La FPU est un peu à part. La '''double émission flottante''' a été utilisée sur les processeurs Alpha. L'idée est d'émettre deux opérations flottantes à la fois. Sauf qu'une implémentation naïve demanderait de dupliquer la FPU, ce qui aurait un cout en circuit rédhibitoire. Pour rappel, les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant. L'idée est d'avoir des ports d'émission séparés pour l'additionneur flottant et le multiplieur flottant. Le processeur peut alors émettre une addition flottante en même temps qu'une multiplication flottante. Les autres circuits de calcul flottant sont répartis sur ces deux ports d'émission flottants. L'avantage est que cela se marie bien avec l'usage d'une fenêtre d'instruction séparée pour les opérations flottantes. La fenêtre d'instruction a alors deux ports séparés, au lieu d'un seul. Rajouter un second port d'émission flottant n'est pas trop un problème, car le cout lié à l'ajout d'un port n'est pas linéaire. Passer de un port à deux a un cout tolérable, bien plus que de passer de 3 ports à 4 ou de 4 à 5. Il est possible de dupliquer l'additionneur et le multiplieur flottant, ce qui permet d'émettre deux additions et multiplications flottantes en même temps. C'est ce qui est fait sur les processeurs AMD d'architecture Zen 1 et 2. Ils ont deux additionneurs flottants par cœur, deux multiplieurs flottants, chacun avec leur propre port d'émission. Les performances en calcul flottant sont assez impressionnantes pour un processeur de l'époque. [[File:ZEN - émission multiple flottante.png|centre|vignette|upright=2|Microarchitecture Zen 1 d'AMD.]] ==Les ports d'émission et de décodage== Les ports d'émission et de décodage sont des ressources matérielles comme les autres. Dans cette section, nous allons parler de concepts liés aux ports d'émission et de décodage. Nous allons notamment parler du partage des ports d'émission, ainsi que des relations numériques entre ces deux types de ports. ===Le partage des ports d'émission et la ''port contention''=== Pour le moment, nous n'avons vu que des cas où un port d'émission est relié soit à des ALU entières, soit à des FPU, pas aux deux. Mais il existe des processeurs où un même port d'émission alimente à la fois une ALU entière et une FPU. Par exemple, on peut relier un additionneur flottant sur le même port qu'une ALU entière. Il faut noter que cela implique une fenêtre d'instruction centralisée, capable de mettre en attente micro-opérations entières et flottantes. Un exemple est celui des processeurs Core 2 Duo. Ils disposent de 6 ports d'émission, dont 3 ports dédiés à l'unité mémoire. Les 3 ports restants alimentent chacun : une ALU entière, un circuit de calcul flottant et une unité de calcul SSE (une unité de calcul SIMD, qu'on abordera dans quelques chapitres). * Le premier port alimente une ALU entière simple et un multiplieur/diviseur flottant. * Le second alimente une ALU entière, un multiplieur entier et un additionneur flottant. * Le troisième alimente une ALU entière, sans circuit flottant dédié. Dans le même genre, il est possible de partager un port d'émission entre l'unité mémoire et une ALU entière. Cela permet d'utiliser l'ALU entière pour les calculs d'adresse, ce qui évite d'avoir à utiliser une unité de calcul d'adresse distincte. Un exemple est celui du processeur superscalaire double émission Power PC 440. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. L'organisation en question permet soit d'émettre un accès mémoire en même temps qu'une opération entière, soit d'émettre deux opérations entières simples, soit d’émettre une multiplication et une addition/soustraction/comparaison. Une organisation simple, mais très efficace ! [[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]] ===La séparation entre largeur de décodage et d'émission=== Sur les processeurs superscalaires larges, le processeur peut émettre plus d'instructions qu'il peut en décoder. Plus haut, j'ai dit que l'unité d'émission avait parfois plus de ports d'émission que de ports de décodage. La raison est assez simple, et elle s'explique assez bien avec un exemple. Prenons un processeur avec trois ALU entières, une FPU, une unité de branchement et une unité mémoire. Chacune a son propre port d'émission, ce qui fait que le CPU est capable d'émettre 6 µops. Du moins, c'est la théorie, car les conditions pour sont assez rares. Il faut qu'en chargeant un paquet de 6 instructions, il y ait exactement trois instructions entières, une flottante, un branchement et un accès mémoire. Si le paquet contient 5 instructions entières et un branchement, seules quatre instructions sont émises. Dans cet exemple, dans la grande majorité des cas, seuls 4 ports seront utilisés en moyenne et deux seront sous-utilisés. Il s'agit là d'une moyenne, qui dépend du code, et des instructions exactes à exécuter. Cependant, cette moyenne nous donne un moyen d'économiser beaucoup de circuits pour un cout en performance mineur. L'idée est de ne charger que des blocs de 4 instructions, mais d'avoir 6 ports d'émission. Le processeur charge et décode 4 instructions, les quatre instructions sont envoyées sur les ports d'émission adéquat. Le cout en transistors est alors situé entre celui d'un CPU quadruple émission et sextuple émission. Et les performances sont aussi entre les deux. Pour les performances, c'est parce que cela permet d'avoir un CPU quadruple émission avec bien moins de contraintes d’appariement. Imaginez qu'on se soit limité à quatre ports d'émission : il aurait fallu mettre plusieurs unités sur le même port d'émission. En conséquence, il y aurait eu des combinaisons interdites, comme émettre deux instructions entières + un branchement + un accès mémoire ; ou deux instructions entières + une opération flottante + un accès mémoire. De tells appariement interdits sont possibles en utilisant les six ports d'émission. ==L'émission parallèle des accès mémoire== 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 simultanné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. De plus, 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. Ce compromis change cependant quand on passe à la quadruple émission. {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 |- | * ALU entière * Multiplieur | * ALU entière * Barrel shifter * Unité de branchement * Unité mémoire | FPU |- | * Additions, soustractions, opérations bit à bit, etc. * Multiplication | * Additions, soustractions, opérations bit à bit, etc. * Décalages et rotations * Branchements * Accès mémoire |Opération flottante |} ===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 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. Par contre, c'est autre chose sur les processeurs superscalaires larges. Vu qu'ils travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares. L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. La ''load/store queue'' reste une structure unique, mais on peut lui rajouter des ports. Et à ce peit jeu, il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire. [[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]] 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. Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre. Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Le Pentium 4 était dans ce cas, mais il était un peu seul dans sa catégorie. Un cas similaire est celui où l'unité mémoire est alimentée par une fenêtre de micro-opération mélangeant instructions mémoire et entières. Les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. Dans ce cas, l'émission multiple est rarement implémentée. Le cas suivant a une file pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique. Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui pouvaient seulement émettre une lecture en même temps qu'une écriture. Ils avaient deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse. [[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]] Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture demande d'utiliser une file d'écriture multiport, ce qui est assez compliqué. Implémenter des lectures en plus est plus simple : il suffit de rajouter une seconde file de µops LOAD et une unité de calcul, alimentées via un second port d'émission. Il est aussi possible de rajouter un second port de lecture au cache, mais ce n'est pas obligatoire. Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse. De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports de lecture et d'écriture car il y a plus de lecture que d'écritures. Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. : Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse. [[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]] Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettent plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail. ==Les optimisations des processeurs superscalaires larges== Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul. ===L'étape de chargement d'un CPU superscalaire large=== Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 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 autoaligné.]] Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements ! [[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]] Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez 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 microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total). Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois 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 microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement. Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations. ===La micro-fusion des processeurs Intel et AMD=== La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''. Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''. Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout. La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Le parallélisme mémoire | prevText=Le parallélisme mémoire | next=Exemples de microarchitectures CPU : le cas du x86 | nextText=Exemples de microarchitectures CPU : le cas du x86 }} </noinclude> 12gp7rff3y9uwu85extxng61nvdocxc 772723 772722 2026-09-21T18:51:36Z Mewtow 31375 /* L'évolution historique des processeurs superscalaires */ 772723 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. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps. 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. 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. Deux branchements consécutifs sont quant à eux plus fréquents, mais exécuter deux branchements en même temps pose un problème : que faire quand les deux sont pris ? Deux accès mémoire consécutifs est assez rare. La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs sont systématiquement supportées, car dupliquer des ALU 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 et 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'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. Par exemple, l'unité de chargement charge deux fois plus d'instructions, mais n'est pas modifiée en profondeur. L'unité d'émission et les circuits d'exécution dans le désordre sont eux aussi altérés. Les unités de calcul sont souvent dupliquées, sauf sur les tout premiers CPU superscalaires. {|class="wikitable" |- ! rowspan="2" | Processeur sans émission multiple | rowspan="2" | Chargement | rowspan="2" | Décodage | rowspan="2" | Renommage | rowspan="2" | Émission | Exécution / ALU | rowspan="2" | ''Commit''/ROB |- | Exécution / ALU |- ! rowspan="4" | Processeur superscalaire | rowspan="4" | Chargement | rowspan="2" | Décodage | rowspan="4" | Renommage | rowspan="4" | Émission | Exécution / ALU | rowspan="4" | ''Commit''/ROB |- | Exécution / ALU |- | rowspan="2" | Décodage | Exécution / ALU |- | Exécution / ALU |} Voyons ce qu'il en est dans le détail. ===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. 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. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant. 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. ===Le chargement de plusieurs instructions consécutives=== Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large. Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence. Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas. Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement non-pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc. [[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]] ===Le décodage parallèle des instructions=== Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits. Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur. Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! ===L'unité d'émission d'un processeur superscalaire=== La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. 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 4 instructions, compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément deux 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. 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. : 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 microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle. 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=== Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement. 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. ==L'évolution historique des processeurs superscalaires== Les tout premiers processeurs superscalaires avaient un budget en transistors très limité, ce qui fait que les contraintes d'appariement étaient très strictes. Et au-delà de ça, ils ne faisaient que mieux utiliser des unités de calcul déjà présentes, ils ne dupliquaient aucune unité de calcul. Prenons un processeur non-superscalaire simple, illustré ci-dessous. Il contient : une unité de chargement, un décodeur, une unité d'émission, une ALU, une FPU, les bancs de registre entier et flottants, et une unité mémoire. Nous avons placé l'unité mémoire à la suite de l'ALU entière, car ce sera important pour les explications qui vont suivre. [[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]] Les processeurs superscalaires historiques modifiaient assez peu le schéma précédent. L'unité de chargement était adaptée pour lire plusieurs instructions depuis la mémoire, il y avait un plusieurs décodeurs, l'unité d'émission était un peu modifiée. Mais aucune unité de calcul n'était dupliquée. A la rigueur, gérer la triple émission avec les branchements demandait parfois d'ajouter des ports de lecture sur le banc de registre généraux, pas plus. L'évolution des CPU superscalaires historiques s'est faite en quatre temps, avec des design de départ simples, qui ont progressivement évolués. Les étapes portent les noms de : double émission entière-flottante, d'exécution parallèle des branchements, d'émission multiple des opérations entières. Après la quatrième étape, les CPU superscalaires sont devenus très variés, au point de ne pas pouvoir être classés simplement. ===La double émission entière-flottante=== Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est d'émettre une micro-opération entière en même temps qu'une micro-opé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, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité. La plupart des premiers processeurs superscalaires étaient de ce type. Les exemples les plus notables sont les processeurs POWER 1 et ses dérivés comme le ''RISC Single Chip''. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle. [[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]] La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisations visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate]. ===L'émission parallèle des branchements=== Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur. 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 modifier le banc de registres. Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. [[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]] Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications et les divisions ? Pour étudier ces questions, nous allons prendre le cas d'un processeur capable d’émettre trois opérations en même temps : deux opérations entières, et une flottante, potentiellement d'autres. {|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 |} 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|Emission multiple des opérations entières, implémentation courante]] Pour les branchements, deux solutions sont possibles. Avec la première, il y a émission parallèle des branchements, à savoir qu'il est possible d'émettre en branchement en parallèle de deux-trois instructions entières. Et cela impose la présence d'une unité de branchement séparée, avec son port d'émission. Avec la seconde, les branchements sont traités comme des instructions entières. Soit les branchements sont exécutés dans une ALU entière, soit il y a une unité de branchement reliée à un port d'émission pour les opérations entières. [[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Emission série et parallèle des branchements]] ===Les CPU superscalaires à plusieurs unités spécialisées=== L'évolution des CPU superscalaires s'est faite en quatre temps, et nous en sommes à la quatrième étape. Et cette quatrième étape ne correspond en réalité pas à grand chose de concret, ni à une évolution technique bien précise. Elle correspond en réalité à l'arrivée de processeur superscalaires très variés, qui n'ont pas grand chose en commun. Le premier CPU superscalaires utilisés dans les PC étaient le Pentium. Et c'était un CPU double émission un peu particulier. Il n'implémentait pas la double émission entière-flottante, mais il avait deux ALU entières. En clair, rien à voir avec une sorte d'évolution des designs précédents. Et il n'est pas le seul, la majorité des CPU superscalaire qui ont suivi ont privilégié l'ajout d'ALU entières. La raison est que les programmes informatiques utilisent beaucoup plus d'additions/soustractions que le reste. Il y a environ 5 opérations entières de base pour une multiplication entière, le ratio est le même quand on remplace la multiplication par un branchement ou un accès mémoire. En conséquence, les ALU entières ont été dupliquées en premier, et dans de nombreux exemplaires. Reste à voir comment a été dupliqué le reste. Les multiplieurs n'ont pas été dupliqués, sauf sur les processeurs superscalaires très larges. La raison est qu'il est rare de devoir exécuter deux multiplications à la fois. Et c'est la même chose pour le ''barrel shifter''. Deux branchements consécutifs sont quant à eux plus fréquents, c'est même assez courant. Aussi, les unités de branchement ont été dupliquées en second lieu, après les ALU entières. Avoir plusieurs accès mémoire proches est assez courant, surtout sur les CPU de type CISC. Aussi, les unités mémoire ont été dupliquées en troisième lieu. Pour résumer, dupliquer des ALU entières est donc la priorité, suivi par la duplication des unités de branchement, puis celle des accès mémoire, pour terminer par le multiplieur et le ''barrel shifter''. La FPU est un peu à part. La '''double émission flottante''' a été utilisée sur les processeurs Alpha. L'idée est d'émettre deux opérations flottantes à la fois. Sauf qu'une implémentation naïve demanderait de dupliquer la FPU, ce qui aurait un cout en circuit rédhibitoire. Pour rappel, les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant. L'idée est d'avoir des ports d'émission séparés pour l'additionneur flottant et le multiplieur flottant. Le processeur peut alors émettre une addition flottante en même temps qu'une multiplication flottante. Les autres circuits de calcul flottant sont répartis sur ces deux ports d'émission flottants. L'avantage est que cela se marie bien avec l'usage d'une fenêtre d'instruction séparée pour les opérations flottantes. La fenêtre d'instruction a alors deux ports séparés, au lieu d'un seul. Rajouter un second port d'émission flottant n'est pas trop un problème, car le cout lié à l'ajout d'un port n'est pas linéaire. Passer de un port à deux a un cout tolérable, bien plus que de passer de 3 ports à 4 ou de 4 à 5. Il est possible de dupliquer l'additionneur et le multiplieur flottant, ce qui permet d'émettre deux additions et multiplications flottantes en même temps. C'est ce qui est fait sur les processeurs AMD d'architecture Zen 1 et 2. Ils ont deux additionneurs flottants par cœur, deux multiplieurs flottants, chacun avec leur propre port d'émission. Les performances en calcul flottant sont assez impressionnantes pour un processeur de l'époque. [[File:ZEN - émission multiple flottante.png|centre|vignette|upright=2|Microarchitecture Zen 1 d'AMD.]] ==Les ports d'émission et de décodage== Les ports d'émission et de décodage sont des ressources matérielles comme les autres. Dans cette section, nous allons parler de concepts liés aux ports d'émission et de décodage. Nous allons notamment parler du partage des ports d'émission, ainsi que des relations numériques entre ces deux types de ports. ===Le partage des ports d'émission et la ''port contention''=== Pour le moment, nous n'avons vu que des cas où un port d'émission est relié soit à des ALU entières, soit à des FPU, pas aux deux. Mais il existe des processeurs où un même port d'émission alimente à la fois une ALU entière et une FPU. Par exemple, on peut relier un additionneur flottant sur le même port qu'une ALU entière. Il faut noter que cela implique une fenêtre d'instruction centralisée, capable de mettre en attente micro-opérations entières et flottantes. Un exemple est celui des processeurs Core 2 Duo. Ils disposent de 6 ports d'émission, dont 3 ports dédiés à l'unité mémoire. Les 3 ports restants alimentent chacun : une ALU entière, un circuit de calcul flottant et une unité de calcul SSE (une unité de calcul SIMD, qu'on abordera dans quelques chapitres). * Le premier port alimente une ALU entière simple et un multiplieur/diviseur flottant. * Le second alimente une ALU entière, un multiplieur entier et un additionneur flottant. * Le troisième alimente une ALU entière, sans circuit flottant dédié. Dans le même genre, il est possible de partager un port d'émission entre l'unité mémoire et une ALU entière. Cela permet d'utiliser l'ALU entière pour les calculs d'adresse, ce qui évite d'avoir à utiliser une unité de calcul d'adresse distincte. Un exemple est celui du processeur superscalaire double émission Power PC 440. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. L'organisation en question permet soit d'émettre un accès mémoire en même temps qu'une opération entière, soit d'émettre deux opérations entières simples, soit d’émettre une multiplication et une addition/soustraction/comparaison. Une organisation simple, mais très efficace ! [[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]] ===La séparation entre largeur de décodage et d'émission=== Sur les processeurs superscalaires larges, le processeur peut émettre plus d'instructions qu'il peut en décoder. Plus haut, j'ai dit que l'unité d'émission avait parfois plus de ports d'émission que de ports de décodage. La raison est assez simple, et elle s'explique assez bien avec un exemple. Prenons un processeur avec trois ALU entières, une FPU, une unité de branchement et une unité mémoire. Chacune a son propre port d'émission, ce qui fait que le CPU est capable d'émettre 6 µops. Du moins, c'est la théorie, car les conditions pour sont assez rares. Il faut qu'en chargeant un paquet de 6 instructions, il y ait exactement trois instructions entières, une flottante, un branchement et un accès mémoire. Si le paquet contient 5 instructions entières et un branchement, seules quatre instructions sont émises. Dans cet exemple, dans la grande majorité des cas, seuls 4 ports seront utilisés en moyenne et deux seront sous-utilisés. Il s'agit là d'une moyenne, qui dépend du code, et des instructions exactes à exécuter. Cependant, cette moyenne nous donne un moyen d'économiser beaucoup de circuits pour un cout en performance mineur. L'idée est de ne charger que des blocs de 4 instructions, mais d'avoir 6 ports d'émission. Le processeur charge et décode 4 instructions, les quatre instructions sont envoyées sur les ports d'émission adéquat. Le cout en transistors est alors situé entre celui d'un CPU quadruple émission et sextuple émission. Et les performances sont aussi entre les deux. Pour les performances, c'est parce que cela permet d'avoir un CPU quadruple émission avec bien moins de contraintes d’appariement. Imaginez qu'on se soit limité à quatre ports d'émission : il aurait fallu mettre plusieurs unités sur le même port d'émission. En conséquence, il y aurait eu des combinaisons interdites, comme émettre deux instructions entières + un branchement + un accès mémoire ; ou deux instructions entières + une opération flottante + un accès mémoire. De tells appariement interdits sont possibles en utilisant les six ports d'émission. ==L'émission parallèle des accès mémoire== 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 simultanné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. De plus, 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. Ce compromis change cependant quand on passe à la quadruple émission. {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 |- | * ALU entière * Multiplieur | * ALU entière * Barrel shifter * Unité de branchement * Unité mémoire | FPU |- | * Additions, soustractions, opérations bit à bit, etc. * Multiplication | * Additions, soustractions, opérations bit à bit, etc. * Décalages et rotations * Branchements * Accès mémoire |Opération flottante |} ===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 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. Par contre, c'est autre chose sur les processeurs superscalaires larges. Vu qu'ils travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares. L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. La ''load/store queue'' reste une structure unique, mais on peut lui rajouter des ports. Et à ce peit jeu, il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire. [[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]] 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. Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre. Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Le Pentium 4 était dans ce cas, mais il était un peu seul dans sa catégorie. Un cas similaire est celui où l'unité mémoire est alimentée par une fenêtre de micro-opération mélangeant instructions mémoire et entières. Les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. Dans ce cas, l'émission multiple est rarement implémentée. Le cas suivant a une file pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique. Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui pouvaient seulement émettre une lecture en même temps qu'une écriture. Ils avaient deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse. [[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]] Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture demande d'utiliser une file d'écriture multiport, ce qui est assez compliqué. Implémenter des lectures en plus est plus simple : il suffit de rajouter une seconde file de µops LOAD et une unité de calcul, alimentées via un second port d'émission. Il est aussi possible de rajouter un second port de lecture au cache, mais ce n'est pas obligatoire. Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse. De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports de lecture et d'écriture car il y a plus de lecture que d'écritures. Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. : Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse. [[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]] Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettent plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail. ==Les optimisations des processeurs superscalaires larges== Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul. ===L'étape de chargement d'un CPU superscalaire large=== Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 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 autoaligné.]] Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements ! [[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]] Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez 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 microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total). Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois 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 microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement. Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations. ===La micro-fusion des processeurs Intel et AMD=== La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''. Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''. Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout. La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Le parallélisme mémoire | prevText=Le parallélisme mémoire | next=Exemples de microarchitectures CPU : le cas du x86 | nextText=Exemples de microarchitectures CPU : le cas du x86 }} </noinclude> p6lwqdi85nxx0jfbea0k2vhzt92e32a 772724 772723 2026-09-21T18:53:27Z Mewtow 31375 /* Les accès mémoire traités dans le pipeline entier */ 772724 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. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps. 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. 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. Deux branchements consécutifs sont quant à eux plus fréquents, mais exécuter deux branchements en même temps pose un problème : que faire quand les deux sont pris ? Deux accès mémoire consécutifs est assez rare. La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs sont systématiquement supportées, car dupliquer des ALU 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 et 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'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. Par exemple, l'unité de chargement charge deux fois plus d'instructions, mais n'est pas modifiée en profondeur. L'unité d'émission et les circuits d'exécution dans le désordre sont eux aussi altérés. Les unités de calcul sont souvent dupliquées, sauf sur les tout premiers CPU superscalaires. {|class="wikitable" |- ! rowspan="2" | Processeur sans émission multiple | rowspan="2" | Chargement | rowspan="2" | Décodage | rowspan="2" | Renommage | rowspan="2" | Émission | Exécution / ALU | rowspan="2" | ''Commit''/ROB |- | Exécution / ALU |- ! rowspan="4" | Processeur superscalaire | rowspan="4" | Chargement | rowspan="2" | Décodage | rowspan="4" | Renommage | rowspan="4" | Émission | Exécution / ALU | rowspan="4" | ''Commit''/ROB |- | Exécution / ALU |- | rowspan="2" | Décodage | Exécution / ALU |- | Exécution / ALU |} Voyons ce qu'il en est dans le détail. ===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. 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. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant. 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. ===Le chargement de plusieurs instructions consécutives=== Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large. Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence. Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas. Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement non-pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc. [[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]] ===Le décodage parallèle des instructions=== Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits. Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur. Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! ===L'unité d'émission d'un processeur superscalaire=== La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. 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 4 instructions, compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément deux 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. 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. : 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 microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle. 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=== Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement. 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. ==L'évolution historique des processeurs superscalaires== Les tout premiers processeurs superscalaires avaient un budget en transistors très limité, ce qui fait que les contraintes d'appariement étaient très strictes. Et au-delà de ça, ils ne faisaient que mieux utiliser des unités de calcul déjà présentes, ils ne dupliquaient aucune unité de calcul. Prenons un processeur non-superscalaire simple, illustré ci-dessous. Il contient : une unité de chargement, un décodeur, une unité d'émission, une ALU, une FPU, les bancs de registre entier et flottants, et une unité mémoire. Nous avons placé l'unité mémoire à la suite de l'ALU entière, car ce sera important pour les explications qui vont suivre. [[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]] Les processeurs superscalaires historiques modifiaient assez peu le schéma précédent. L'unité de chargement était adaptée pour lire plusieurs instructions depuis la mémoire, il y avait un plusieurs décodeurs, l'unité d'émission était un peu modifiée. Mais aucune unité de calcul n'était dupliquée. A la rigueur, gérer la triple émission avec les branchements demandait parfois d'ajouter des ports de lecture sur le banc de registre généraux, pas plus. L'évolution des CPU superscalaires historiques s'est faite en quatre temps, avec des design de départ simples, qui ont progressivement évolués. Les étapes portent les noms de : double émission entière-flottante, d'exécution parallèle des branchements, d'émission multiple des opérations entières. Après la quatrième étape, les CPU superscalaires sont devenus très variés, au point de ne pas pouvoir être classés simplement. ===La double émission entière-flottante=== Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est d'émettre une micro-opération entière en même temps qu'une micro-opé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, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité. La plupart des premiers processeurs superscalaires étaient de ce type. Les exemples les plus notables sont les processeurs POWER 1 et ses dérivés comme le ''RISC Single Chip''. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle. [[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]] La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisations visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate]. ===L'émission parallèle des branchements=== Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur. 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 modifier le banc de registres. Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. [[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]] Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications et les divisions ? Pour étudier ces questions, nous allons prendre le cas d'un processeur capable d’émettre trois opérations en même temps : deux opérations entières, et une flottante, potentiellement d'autres. {|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 |} 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|Emission multiple des opérations entières, implémentation courante]] Pour les branchements, deux solutions sont possibles. Avec la première, il y a émission parallèle des branchements, à savoir qu'il est possible d'émettre en branchement en parallèle de deux-trois instructions entières. Et cela impose la présence d'une unité de branchement séparée, avec son port d'émission. Avec la seconde, les branchements sont traités comme des instructions entières. Soit les branchements sont exécutés dans une ALU entière, soit il y a une unité de branchement reliée à un port d'émission pour les opérations entières. [[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Emission série et parallèle des branchements]] ===Les CPU superscalaires à plusieurs unités spécialisées=== L'évolution des CPU superscalaires s'est faite en quatre temps, et nous en sommes à la quatrième étape. Et cette quatrième étape ne correspond en réalité pas à grand chose de concret, ni à une évolution technique bien précise. Elle correspond en réalité à l'arrivée de processeur superscalaires très variés, qui n'ont pas grand chose en commun. Le premier CPU superscalaires utilisés dans les PC étaient le Pentium. Et c'était un CPU double émission un peu particulier. Il n'implémentait pas la double émission entière-flottante, mais il avait deux ALU entières. En clair, rien à voir avec une sorte d'évolution des designs précédents. Et il n'est pas le seul, la majorité des CPU superscalaire qui ont suivi ont privilégié l'ajout d'ALU entières. La raison est que les programmes informatiques utilisent beaucoup plus d'additions/soustractions que le reste. Il y a environ 5 opérations entières de base pour une multiplication entière, le ratio est le même quand on remplace la multiplication par un branchement ou un accès mémoire. En conséquence, les ALU entières ont été dupliquées en premier, et dans de nombreux exemplaires. Reste à voir comment a été dupliqué le reste. Les multiplieurs n'ont pas été dupliqués, sauf sur les processeurs superscalaires très larges. La raison est qu'il est rare de devoir exécuter deux multiplications à la fois. Et c'est la même chose pour le ''barrel shifter''. Deux branchements consécutifs sont quant à eux plus fréquents, c'est même assez courant. Aussi, les unités de branchement ont été dupliquées en second lieu, après les ALU entières. Avoir plusieurs accès mémoire proches est assez courant, surtout sur les CPU de type CISC. Aussi, les unités mémoire ont été dupliquées en troisième lieu. Pour résumer, dupliquer des ALU entières est donc la priorité, suivi par la duplication des unités de branchement, puis celle des accès mémoire, pour terminer par le multiplieur et le ''barrel shifter''. La FPU est un peu à part. La '''double émission flottante''' a été utilisée sur les processeurs Alpha. L'idée est d'émettre deux opérations flottantes à la fois. Sauf qu'une implémentation naïve demanderait de dupliquer la FPU, ce qui aurait un cout en circuit rédhibitoire. Pour rappel, les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant. L'idée est d'avoir des ports d'émission séparés pour l'additionneur flottant et le multiplieur flottant. Le processeur peut alors émettre une addition flottante en même temps qu'une multiplication flottante. Les autres circuits de calcul flottant sont répartis sur ces deux ports d'émission flottants. L'avantage est que cela se marie bien avec l'usage d'une fenêtre d'instruction séparée pour les opérations flottantes. La fenêtre d'instruction a alors deux ports séparés, au lieu d'un seul. Rajouter un second port d'émission flottant n'est pas trop un problème, car le cout lié à l'ajout d'un port n'est pas linéaire. Passer de un port à deux a un cout tolérable, bien plus que de passer de 3 ports à 4 ou de 4 à 5. Il est possible de dupliquer l'additionneur et le multiplieur flottant, ce qui permet d'émettre deux additions et multiplications flottantes en même temps. C'est ce qui est fait sur les processeurs AMD d'architecture Zen 1 et 2. Ils ont deux additionneurs flottants par cœur, deux multiplieurs flottants, chacun avec leur propre port d'émission. Les performances en calcul flottant sont assez impressionnantes pour un processeur de l'époque. [[File:ZEN - émission multiple flottante.png|centre|vignette|upright=2|Microarchitecture Zen 1 d'AMD.]] ==Les ports d'émission et de décodage== Les ports d'émission et de décodage sont des ressources matérielles comme les autres. Dans cette section, nous allons parler de concepts liés aux ports d'émission et de décodage. Nous allons notamment parler du partage des ports d'émission, ainsi que des relations numériques entre ces deux types de ports. ===Le partage des ports d'émission et la ''port contention''=== Pour le moment, nous n'avons vu que des cas où un port d'émission est relié soit à des ALU entières, soit à des FPU, pas aux deux. Mais il existe des processeurs où un même port d'émission alimente à la fois une ALU entière et une FPU. Par exemple, on peut relier un additionneur flottant sur le même port qu'une ALU entière. Il faut noter que cela implique une fenêtre d'instruction centralisée, capable de mettre en attente micro-opérations entières et flottantes. Un exemple est celui des processeurs Core 2 Duo. Ils disposent de 6 ports d'émission, dont 3 ports dédiés à l'unité mémoire. Les 3 ports restants alimentent chacun : une ALU entière, un circuit de calcul flottant et une unité de calcul SSE (une unité de calcul SIMD, qu'on abordera dans quelques chapitres). * Le premier port alimente une ALU entière simple et un multiplieur/diviseur flottant. * Le second alimente une ALU entière, un multiplieur entier et un additionneur flottant. * Le troisième alimente une ALU entière, sans circuit flottant dédié. Dans le même genre, il est possible de partager un port d'émission entre l'unité mémoire et une ALU entière. Cela permet d'utiliser l'ALU entière pour les calculs d'adresse, ce qui évite d'avoir à utiliser une unité de calcul d'adresse distincte. Un exemple est celui du processeur superscalaire double émission Power PC 440. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. L'organisation en question permet soit d'émettre un accès mémoire en même temps qu'une opération entière, soit d'émettre deux opérations entières simples, soit d’émettre une multiplication et une addition/soustraction/comparaison. Une organisation simple, mais très efficace ! [[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]] ===La séparation entre largeur de décodage et d'émission=== Sur les processeurs superscalaires larges, le processeur peut émettre plus d'instructions qu'il peut en décoder. Plus haut, j'ai dit que l'unité d'émission avait parfois plus de ports d'émission que de ports de décodage. La raison est assez simple, et elle s'explique assez bien avec un exemple. Prenons un processeur avec trois ALU entières, une FPU, une unité de branchement et une unité mémoire. Chacune a son propre port d'émission, ce qui fait que le CPU est capable d'émettre 6 µops. Du moins, c'est la théorie, car les conditions pour sont assez rares. Il faut qu'en chargeant un paquet de 6 instructions, il y ait exactement trois instructions entières, une flottante, un branchement et un accès mémoire. Si le paquet contient 5 instructions entières et un branchement, seules quatre instructions sont émises. Dans cet exemple, dans la grande majorité des cas, seuls 4 ports seront utilisés en moyenne et deux seront sous-utilisés. Il s'agit là d'une moyenne, qui dépend du code, et des instructions exactes à exécuter. Cependant, cette moyenne nous donne un moyen d'économiser beaucoup de circuits pour un cout en performance mineur. L'idée est de ne charger que des blocs de 4 instructions, mais d'avoir 6 ports d'émission. Le processeur charge et décode 4 instructions, les quatre instructions sont envoyées sur les ports d'émission adéquat. Le cout en transistors est alors situé entre celui d'un CPU quadruple émission et sextuple émission. Et les performances sont aussi entre les deux. Pour les performances, c'est parce que cela permet d'avoir un CPU quadruple émission avec bien moins de contraintes d’appariement. Imaginez qu'on se soit limité à quatre ports d'émission : il aurait fallu mettre plusieurs unités sur le même port d'émission. En conséquence, il y aurait eu des combinaisons interdites, comme émettre deux instructions entières + un branchement + un accès mémoire ; ou deux instructions entières + une opération flottante + un accès mémoire. De tells appariement interdits sont possibles en utilisant les six ports d'émission. ==L'émission parallèle des accès mémoire== 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 simultanné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 à doubvle émission entière-flottante. [[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]] De plus, 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. Ce compromis change cependant quand on passe à la quadruple émission. {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 |- | * ALU entière * Multiplieur | * ALU entière * Barrel shifter * Unité de branchement * Unité mémoire | FPU |- | * Additions, soustractions, opérations bit à bit, etc. * Multiplication | * Additions, soustractions, opérations bit à bit, etc. * Décalages et rotations * Branchements * Accès mémoire |Opération flottante |} ===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 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. Par contre, c'est autre chose sur les processeurs superscalaires larges. Vu qu'ils travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares. L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. La ''load/store queue'' reste une structure unique, mais on peut lui rajouter des ports. Et à ce peit jeu, il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire. [[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]] 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. Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre. Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Le Pentium 4 était dans ce cas, mais il était un peu seul dans sa catégorie. Un cas similaire est celui où l'unité mémoire est alimentée par une fenêtre de micro-opération mélangeant instructions mémoire et entières. Les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. Dans ce cas, l'émission multiple est rarement implémentée. Le cas suivant a une file pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique. Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui pouvaient seulement émettre une lecture en même temps qu'une écriture. Ils avaient deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse. [[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]] Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture demande d'utiliser une file d'écriture multiport, ce qui est assez compliqué. Implémenter des lectures en plus est plus simple : il suffit de rajouter une seconde file de µops LOAD et une unité de calcul, alimentées via un second port d'émission. Il est aussi possible de rajouter un second port de lecture au cache, mais ce n'est pas obligatoire. Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse. De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports de lecture et d'écriture car il y a plus de lecture que d'écritures. Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. : Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse. [[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]] Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettent plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail. ==Les optimisations des processeurs superscalaires larges== Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul. ===L'étape de chargement d'un CPU superscalaire large=== Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 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 autoaligné.]] Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements ! [[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]] Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez 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 microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total). Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois 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 microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement. Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations. ===La micro-fusion des processeurs Intel et AMD=== La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''. Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''. Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout. La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Le parallélisme mémoire | prevText=Le parallélisme mémoire | next=Exemples de microarchitectures CPU : le cas du x86 | nextText=Exemples de microarchitectures CPU : le cas du x86 }} </noinclude> 4oz0r1ya4w01958legvs5wsmcdoru4v 772725 772724 2026-09-21T18:58:13Z Mewtow 31375 /* Les accès mémoire traités dans le pipeline entier */ 772725 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. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps. 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. 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. Deux branchements consécutifs sont quant à eux plus fréquents, mais exécuter deux branchements en même temps pose un problème : que faire quand les deux sont pris ? Deux accès mémoire consécutifs est assez rare. La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs sont systématiquement supportées, car dupliquer des ALU 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 et 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'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. Par exemple, l'unité de chargement charge deux fois plus d'instructions, mais n'est pas modifiée en profondeur. L'unité d'émission et les circuits d'exécution dans le désordre sont eux aussi altérés. Les unités de calcul sont souvent dupliquées, sauf sur les tout premiers CPU superscalaires. {|class="wikitable" |- ! rowspan="2" | Processeur sans émission multiple | rowspan="2" | Chargement | rowspan="2" | Décodage | rowspan="2" | Renommage | rowspan="2" | Émission | Exécution / ALU | rowspan="2" | ''Commit''/ROB |- | Exécution / ALU |- ! rowspan="4" | Processeur superscalaire | rowspan="4" | Chargement | rowspan="2" | Décodage | rowspan="4" | Renommage | rowspan="4" | Émission | Exécution / ALU | rowspan="4" | ''Commit''/ROB |- | Exécution / ALU |- | rowspan="2" | Décodage | Exécution / ALU |- | Exécution / ALU |} Voyons ce qu'il en est dans le détail. ===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. 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. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant. 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. ===Le chargement de plusieurs instructions consécutives=== Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large. Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence. Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas. Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement non-pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc. [[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]] ===Le décodage parallèle des instructions=== Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits. Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur. Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! ===L'unité d'émission d'un processeur superscalaire=== La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. 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 4 instructions, compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément deux 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. 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. : 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 microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle. 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=== Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement. 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. ==L'évolution historique des processeurs superscalaires== Les tout premiers processeurs superscalaires avaient un budget en transistors très limité, ce qui fait que les contraintes d'appariement étaient très strictes. Et au-delà de ça, ils ne faisaient que mieux utiliser des unités de calcul déjà présentes, ils ne dupliquaient aucune unité de calcul. Prenons un processeur non-superscalaire simple, illustré ci-dessous. Il contient : une unité de chargement, un décodeur, une unité d'émission, une ALU, une FPU, les bancs de registre entier et flottants, et une unité mémoire. Nous avons placé l'unité mémoire à la suite de l'ALU entière, car ce sera important pour les explications qui vont suivre. [[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]] Les processeurs superscalaires historiques modifiaient assez peu le schéma précédent. L'unité de chargement était adaptée pour lire plusieurs instructions depuis la mémoire, il y avait un plusieurs décodeurs, l'unité d'émission était un peu modifiée. Mais aucune unité de calcul n'était dupliquée. A la rigueur, gérer la triple émission avec les branchements demandait parfois d'ajouter des ports de lecture sur le banc de registre généraux, pas plus. L'évolution des CPU superscalaires historiques s'est faite en quatre temps, avec des design de départ simples, qui ont progressivement évolués. Les étapes portent les noms de : double émission entière-flottante, d'exécution parallèle des branchements, d'émission multiple des opérations entières. Après la quatrième étape, les CPU superscalaires sont devenus très variés, au point de ne pas pouvoir être classés simplement. ===La double émission entière-flottante=== Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est d'émettre une micro-opération entière en même temps qu'une micro-opé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, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité. La plupart des premiers processeurs superscalaires étaient de ce type. Les exemples les plus notables sont les processeurs POWER 1 et ses dérivés comme le ''RISC Single Chip''. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle. [[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]] La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisations visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate]. ===L'émission parallèle des branchements=== Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur. 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 modifier le banc de registres. Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. [[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]] Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications et les divisions ? Pour étudier ces questions, nous allons prendre le cas d'un processeur capable d’émettre trois opérations en même temps : deux opérations entières, et une flottante, potentiellement d'autres. {|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 |} 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|Emission multiple des opérations entières, implémentation courante]] Pour les branchements, deux solutions sont possibles. Avec la première, il y a émission parallèle des branchements, à savoir qu'il est possible d'émettre en branchement en parallèle de deux-trois instructions entières. Et cela impose la présence d'une unité de branchement séparée, avec son port d'émission. Avec la seconde, les branchements sont traités comme des instructions entières. Soit les branchements sont exécutés dans une ALU entière, soit il y a une unité de branchement reliée à un port d'émission pour les opérations entières. [[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Emission série et parallèle des branchements]] ===Les CPU superscalaires à plusieurs unités spécialisées=== L'évolution des CPU superscalaires s'est faite en quatre temps, et nous en sommes à la quatrième étape. Et cette quatrième étape ne correspond en réalité pas à grand chose de concret, ni à une évolution technique bien précise. Elle correspond en réalité à l'arrivée de processeur superscalaires très variés, qui n'ont pas grand chose en commun. Le premier CPU superscalaires utilisés dans les PC étaient le Pentium. Et c'était un CPU double émission un peu particulier. Il n'implémentait pas la double émission entière-flottante, mais il avait deux ALU entières. En clair, rien à voir avec une sorte d'évolution des designs précédents. Et il n'est pas le seul, la majorité des CPU superscalaire qui ont suivi ont privilégié l'ajout d'ALU entières. La raison est que les programmes informatiques utilisent beaucoup plus d'additions/soustractions que le reste. Il y a environ 5 opérations entières de base pour une multiplication entière, le ratio est le même quand on remplace la multiplication par un branchement ou un accès mémoire. En conséquence, les ALU entières ont été dupliquées en premier, et dans de nombreux exemplaires. Reste à voir comment a été dupliqué le reste. Les multiplieurs n'ont pas été dupliqués, sauf sur les processeurs superscalaires très larges. La raison est qu'il est rare de devoir exécuter deux multiplications à la fois. Et c'est la même chose pour le ''barrel shifter''. Deux branchements consécutifs sont quant à eux plus fréquents, c'est même assez courant. Aussi, les unités de branchement ont été dupliquées en second lieu, après les ALU entières. Avoir plusieurs accès mémoire proches est assez courant, surtout sur les CPU de type CISC. Aussi, les unités mémoire ont été dupliquées en troisième lieu. Pour résumer, dupliquer des ALU entières est donc la priorité, suivi par la duplication des unités de branchement, puis celle des accès mémoire, pour terminer par le multiplieur et le ''barrel shifter''. La FPU est un peu à part. La '''double émission flottante''' a été utilisée sur les processeurs Alpha. L'idée est d'émettre deux opérations flottantes à la fois. Sauf qu'une implémentation naïve demanderait de dupliquer la FPU, ce qui aurait un cout en circuit rédhibitoire. Pour rappel, les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant. L'idée est d'avoir des ports d'émission séparés pour l'additionneur flottant et le multiplieur flottant. Le processeur peut alors émettre une addition flottante en même temps qu'une multiplication flottante. Les autres circuits de calcul flottant sont répartis sur ces deux ports d'émission flottants. L'avantage est que cela se marie bien avec l'usage d'une fenêtre d'instruction séparée pour les opérations flottantes. La fenêtre d'instruction a alors deux ports séparés, au lieu d'un seul. Rajouter un second port d'émission flottant n'est pas trop un problème, car le cout lié à l'ajout d'un port n'est pas linéaire. Passer de un port à deux a un cout tolérable, bien plus que de passer de 3 ports à 4 ou de 4 à 5. Il est possible de dupliquer l'additionneur et le multiplieur flottant, ce qui permet d'émettre deux additions et multiplications flottantes en même temps. C'est ce qui est fait sur les processeurs AMD d'architecture Zen 1 et 2. Ils ont deux additionneurs flottants par cœur, deux multiplieurs flottants, chacun avec leur propre port d'émission. Les performances en calcul flottant sont assez impressionnantes pour un processeur de l'époque. [[File:ZEN - émission multiple flottante.png|centre|vignette|upright=2|Microarchitecture Zen 1 d'AMD.]] ==Les ports d'émission et de décodage== Les ports d'émission et de décodage sont des ressources matérielles comme les autres. Dans cette section, nous allons parler de concepts liés aux ports d'émission et de décodage. Nous allons notamment parler du partage des ports d'émission, ainsi que des relations numériques entre ces deux types de ports. ===Le partage des ports d'émission et la ''port contention''=== Pour le moment, nous n'avons vu que des cas où un port d'émission est relié soit à des ALU entières, soit à des FPU, pas aux deux. Mais il existe des processeurs où un même port d'émission alimente à la fois une ALU entière et une FPU. Par exemple, on peut relier un additionneur flottant sur le même port qu'une ALU entière. Il faut noter que cela implique une fenêtre d'instruction centralisée, capable de mettre en attente micro-opérations entières et flottantes. Un exemple est celui des processeurs Core 2 Duo. Ils disposent de 6 ports d'émission, dont 3 ports dédiés à l'unité mémoire. Les 3 ports restants alimentent chacun : une ALU entière, un circuit de calcul flottant et une unité de calcul SSE (une unité de calcul SIMD, qu'on abordera dans quelques chapitres). * Le premier port alimente une ALU entière simple et un multiplieur/diviseur flottant. * Le second alimente une ALU entière, un multiplieur entier et un additionneur flottant. * Le troisième alimente une ALU entière, sans circuit flottant dédié. Dans le même genre, il est possible de partager un port d'émission entre l'unité mémoire et une ALU entière. Cela permet d'utiliser l'ALU entière pour les calculs d'adresse, ce qui évite d'avoir à utiliser une unité de calcul d'adresse distincte. Un exemple est celui du processeur superscalaire double émission Power PC 440. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. L'organisation en question permet soit d'émettre un accès mémoire en même temps qu'une opération entière, soit d'émettre deux opérations entières simples, soit d’émettre une multiplication et une addition/soustraction/comparaison. Une organisation simple, mais très efficace ! [[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]] ===La séparation entre largeur de décodage et d'émission=== Sur les processeurs superscalaires larges, le processeur peut émettre plus d'instructions qu'il peut en décoder. Plus haut, j'ai dit que l'unité d'émission avait parfois plus de ports d'émission que de ports de décodage. La raison est assez simple, et elle s'explique assez bien avec un exemple. Prenons un processeur avec trois ALU entières, une FPU, une unité de branchement et une unité mémoire. Chacune a son propre port d'émission, ce qui fait que le CPU est capable d'émettre 6 µops. Du moins, c'est la théorie, car les conditions pour sont assez rares. Il faut qu'en chargeant un paquet de 6 instructions, il y ait exactement trois instructions entières, une flottante, un branchement et un accès mémoire. Si le paquet contient 5 instructions entières et un branchement, seules quatre instructions sont émises. Dans cet exemple, dans la grande majorité des cas, seuls 4 ports seront utilisés en moyenne et deux seront sous-utilisés. Il s'agit là d'une moyenne, qui dépend du code, et des instructions exactes à exécuter. Cependant, cette moyenne nous donne un moyen d'économiser beaucoup de circuits pour un cout en performance mineur. L'idée est de ne charger que des blocs de 4 instructions, mais d'avoir 6 ports d'émission. Le processeur charge et décode 4 instructions, les quatre instructions sont envoyées sur les ports d'émission adéquat. Le cout en transistors est alors situé entre celui d'un CPU quadruple émission et sextuple émission. Et les performances sont aussi entre les deux. Pour les performances, c'est parce que cela permet d'avoir un CPU quadruple émission avec bien moins de contraintes d’appariement. Imaginez qu'on se soit limité à quatre ports d'émission : il aurait fallu mettre plusieurs unités sur le même port d'émission. En conséquence, il y aurait eu des combinaisons interdites, comme émettre deux instructions entières + un branchement + un accès mémoire ; ou deux instructions entières + une opération flottante + un accès mémoire. De tells appariement interdits sont possibles en utilisant les six ports d'émission. ==L'émission parallèle des accès mémoire== 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 simultanné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 à doubvle émission entière-flottante. [[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]] 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. Ce compromis change cependant quand on passe à la quadruple émission. ===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 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. Par contre, c'est autre chose sur les processeurs superscalaires larges. Vu qu'ils travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares. L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. La ''load/store queue'' reste une structure unique, mais on peut lui rajouter des ports. Et à ce peit jeu, il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire. [[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]] 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. Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre. Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Le Pentium 4 était dans ce cas, mais il était un peu seul dans sa catégorie. Un cas similaire est celui où l'unité mémoire est alimentée par une fenêtre de micro-opération mélangeant instructions mémoire et entières. Les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. Dans ce cas, l'émission multiple est rarement implémentée. Le cas suivant a une file pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique. Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui pouvaient seulement émettre une lecture en même temps qu'une écriture. Ils avaient deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse. [[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]] Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture demande d'utiliser une file d'écriture multiport, ce qui est assez compliqué. Implémenter des lectures en plus est plus simple : il suffit de rajouter une seconde file de µops LOAD et une unité de calcul, alimentées via un second port d'émission. Il est aussi possible de rajouter un second port de lecture au cache, mais ce n'est pas obligatoire. Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse. De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports de lecture et d'écriture car il y a plus de lecture que d'écritures. Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. : Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse. [[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]] Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettent plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail. ==Les optimisations des processeurs superscalaires larges== Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul. ===L'étape de chargement d'un CPU superscalaire large=== Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 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 autoaligné.]] Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements ! [[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]] Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez 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 microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total). Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois 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 microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement. Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations. ===La micro-fusion des processeurs Intel et AMD=== La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''. Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''. Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout. La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Le parallélisme mémoire | prevText=Le parallélisme mémoire | next=Exemples de microarchitectures CPU : le cas du x86 | nextText=Exemples de microarchitectures CPU : le cas du x86 }} </noinclude> 6a6jq0w0q1t7qit7qhct4mb06vqhytl 772726 772725 2026-09-21T18:58:29Z Mewtow 31375 /* L'émission multiple des micro-opérations entières */ 772726 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. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps. 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. 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. Deux branchements consécutifs sont quant à eux plus fréquents, mais exécuter deux branchements en même temps pose un problème : que faire quand les deux sont pris ? Deux accès mémoire consécutifs est assez rare. La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs sont systématiquement supportées, car dupliquer des ALU 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 et 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'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. Par exemple, l'unité de chargement charge deux fois plus d'instructions, mais n'est pas modifiée en profondeur. L'unité d'émission et les circuits d'exécution dans le désordre sont eux aussi altérés. Les unités de calcul sont souvent dupliquées, sauf sur les tout premiers CPU superscalaires. {|class="wikitable" |- ! rowspan="2" | Processeur sans émission multiple | rowspan="2" | Chargement | rowspan="2" | Décodage | rowspan="2" | Renommage | rowspan="2" | Émission | Exécution / ALU | rowspan="2" | ''Commit''/ROB |- | Exécution / ALU |- ! rowspan="4" | Processeur superscalaire | rowspan="4" | Chargement | rowspan="2" | Décodage | rowspan="4" | Renommage | rowspan="4" | Émission | Exécution / ALU | rowspan="4" | ''Commit''/ROB |- | Exécution / ALU |- | rowspan="2" | Décodage | Exécution / ALU |- | Exécution / ALU |} Voyons ce qu'il en est dans le détail. ===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. 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. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant. 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. ===Le chargement de plusieurs instructions consécutives=== Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large. Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence. Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas. Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement non-pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc. [[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]] ===Le décodage parallèle des instructions=== Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits. Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur. Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! ===L'unité d'émission d'un processeur superscalaire=== La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. 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 4 instructions, compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément deux 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. 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. : 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 microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle. 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=== Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement. 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. ==L'évolution historique des processeurs superscalaires== Les tout premiers processeurs superscalaires avaient un budget en transistors très limité, ce qui fait que les contraintes d'appariement étaient très strictes. Et au-delà de ça, ils ne faisaient que mieux utiliser des unités de calcul déjà présentes, ils ne dupliquaient aucune unité de calcul. Prenons un processeur non-superscalaire simple, illustré ci-dessous. Il contient : une unité de chargement, un décodeur, une unité d'émission, une ALU, une FPU, les bancs de registre entier et flottants, et une unité mémoire. Nous avons placé l'unité mémoire à la suite de l'ALU entière, car ce sera important pour les explications qui vont suivre. [[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]] Les processeurs superscalaires historiques modifiaient assez peu le schéma précédent. L'unité de chargement était adaptée pour lire plusieurs instructions depuis la mémoire, il y avait un plusieurs décodeurs, l'unité d'émission était un peu modifiée. Mais aucune unité de calcul n'était dupliquée. A la rigueur, gérer la triple émission avec les branchements demandait parfois d'ajouter des ports de lecture sur le banc de registre généraux, pas plus. L'évolution des CPU superscalaires historiques s'est faite en quatre temps, avec des design de départ simples, qui ont progressivement évolués. Les étapes portent les noms de : double émission entière-flottante, d'exécution parallèle des branchements, d'émission multiple des opérations entières. Après la quatrième étape, les CPU superscalaires sont devenus très variés, au point de ne pas pouvoir être classés simplement. ===La double émission entière-flottante=== Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est d'émettre une micro-opération entière en même temps qu'une micro-opé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, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité. La plupart des premiers processeurs superscalaires étaient de ce type. Les exemples les plus notables sont les processeurs POWER 1 et ses dérivés comme le ''RISC Single Chip''. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle. [[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]] La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisations visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate]. ===L'émission parallèle des branchements=== Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur. 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 modifier le banc de registres. Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. [[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]] Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications et les divisions ? Pour étudier ces questions, nous allons prendre le cas d'un processeur capable d’émettre trois opérations en même temps : deux opérations entières, et une flottante, potentiellement d'autres. {|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 |} 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|Emission multiple des opérations entières, implémentation courante]] Pour les branchements, deux solutions sont possibles. Avec la première, il y a émission parallèle des branchements, à savoir qu'il est possible d'émettre en branchement en parallèle de deux-trois instructions entières. Et cela impose la présence d'une unité de branchement séparée, avec son port d'émission. Avec la seconde, les branchements sont traités comme des instructions entières. Soit les branchements sont exécutés dans une ALU entière, soit il y a une unité de branchement reliée à un port d'émission pour les opérations entières. [[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Emission série et parallèle des branchements]] {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 |- | * ALU entière * Multiplieur | * ALU entière * Barrel shifter * Unité de branchement | FPU |- | * Additions, soustractions, opérations bit à bit, etc. * Multiplication | * Additions, soustractions, opérations bit à bit, etc. * Décalages et rotations * Branchements |Opération flottante |} ===Les CPU superscalaires à plusieurs unités spécialisées=== L'évolution des CPU superscalaires s'est faite en quatre temps, et nous en sommes à la quatrième étape. Et cette quatrième étape ne correspond en réalité pas à grand chose de concret, ni à une évolution technique bien précise. Elle correspond en réalité à l'arrivée de processeur superscalaires très variés, qui n'ont pas grand chose en commun. Le premier CPU superscalaires utilisés dans les PC étaient le Pentium. Et c'était un CPU double émission un peu particulier. Il n'implémentait pas la double émission entière-flottante, mais il avait deux ALU entières. En clair, rien à voir avec une sorte d'évolution des designs précédents. Et il n'est pas le seul, la majorité des CPU superscalaire qui ont suivi ont privilégié l'ajout d'ALU entières. La raison est que les programmes informatiques utilisent beaucoup plus d'additions/soustractions que le reste. Il y a environ 5 opérations entières de base pour une multiplication entière, le ratio est le même quand on remplace la multiplication par un branchement ou un accès mémoire. En conséquence, les ALU entières ont été dupliquées en premier, et dans de nombreux exemplaires. Reste à voir comment a été dupliqué le reste. Les multiplieurs n'ont pas été dupliqués, sauf sur les processeurs superscalaires très larges. La raison est qu'il est rare de devoir exécuter deux multiplications à la fois. Et c'est la même chose pour le ''barrel shifter''. Deux branchements consécutifs sont quant à eux plus fréquents, c'est même assez courant. Aussi, les unités de branchement ont été dupliquées en second lieu, après les ALU entières. Avoir plusieurs accès mémoire proches est assez courant, surtout sur les CPU de type CISC. Aussi, les unités mémoire ont été dupliquées en troisième lieu. Pour résumer, dupliquer des ALU entières est donc la priorité, suivi par la duplication des unités de branchement, puis celle des accès mémoire, pour terminer par le multiplieur et le ''barrel shifter''. La FPU est un peu à part. La '''double émission flottante''' a été utilisée sur les processeurs Alpha. L'idée est d'émettre deux opérations flottantes à la fois. Sauf qu'une implémentation naïve demanderait de dupliquer la FPU, ce qui aurait un cout en circuit rédhibitoire. Pour rappel, les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant. L'idée est d'avoir des ports d'émission séparés pour l'additionneur flottant et le multiplieur flottant. Le processeur peut alors émettre une addition flottante en même temps qu'une multiplication flottante. Les autres circuits de calcul flottant sont répartis sur ces deux ports d'émission flottants. L'avantage est que cela se marie bien avec l'usage d'une fenêtre d'instruction séparée pour les opérations flottantes. La fenêtre d'instruction a alors deux ports séparés, au lieu d'un seul. Rajouter un second port d'émission flottant n'est pas trop un problème, car le cout lié à l'ajout d'un port n'est pas linéaire. Passer de un port à deux a un cout tolérable, bien plus que de passer de 3 ports à 4 ou de 4 à 5. Il est possible de dupliquer l'additionneur et le multiplieur flottant, ce qui permet d'émettre deux additions et multiplications flottantes en même temps. C'est ce qui est fait sur les processeurs AMD d'architecture Zen 1 et 2. Ils ont deux additionneurs flottants par cœur, deux multiplieurs flottants, chacun avec leur propre port d'émission. Les performances en calcul flottant sont assez impressionnantes pour un processeur de l'époque. [[File:ZEN - émission multiple flottante.png|centre|vignette|upright=2|Microarchitecture Zen 1 d'AMD.]] ==Les ports d'émission et de décodage== Les ports d'émission et de décodage sont des ressources matérielles comme les autres. Dans cette section, nous allons parler de concepts liés aux ports d'émission et de décodage. Nous allons notamment parler du partage des ports d'émission, ainsi que des relations numériques entre ces deux types de ports. ===Le partage des ports d'émission et la ''port contention''=== Pour le moment, nous n'avons vu que des cas où un port d'émission est relié soit à des ALU entières, soit à des FPU, pas aux deux. Mais il existe des processeurs où un même port d'émission alimente à la fois une ALU entière et une FPU. Par exemple, on peut relier un additionneur flottant sur le même port qu'une ALU entière. Il faut noter que cela implique une fenêtre d'instruction centralisée, capable de mettre en attente micro-opérations entières et flottantes. Un exemple est celui des processeurs Core 2 Duo. Ils disposent de 6 ports d'émission, dont 3 ports dédiés à l'unité mémoire. Les 3 ports restants alimentent chacun : une ALU entière, un circuit de calcul flottant et une unité de calcul SSE (une unité de calcul SIMD, qu'on abordera dans quelques chapitres). * Le premier port alimente une ALU entière simple et un multiplieur/diviseur flottant. * Le second alimente une ALU entière, un multiplieur entier et un additionneur flottant. * Le troisième alimente une ALU entière, sans circuit flottant dédié. Dans le même genre, il est possible de partager un port d'émission entre l'unité mémoire et une ALU entière. Cela permet d'utiliser l'ALU entière pour les calculs d'adresse, ce qui évite d'avoir à utiliser une unité de calcul d'adresse distincte. Un exemple est celui du processeur superscalaire double émission Power PC 440. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. L'organisation en question permet soit d'émettre un accès mémoire en même temps qu'une opération entière, soit d'émettre deux opérations entières simples, soit d’émettre une multiplication et une addition/soustraction/comparaison. Une organisation simple, mais très efficace ! [[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]] ===La séparation entre largeur de décodage et d'émission=== Sur les processeurs superscalaires larges, le processeur peut émettre plus d'instructions qu'il peut en décoder. Plus haut, j'ai dit que l'unité d'émission avait parfois plus de ports d'émission que de ports de décodage. La raison est assez simple, et elle s'explique assez bien avec un exemple. Prenons un processeur avec trois ALU entières, une FPU, une unité de branchement et une unité mémoire. Chacune a son propre port d'émission, ce qui fait que le CPU est capable d'émettre 6 µops. Du moins, c'est la théorie, car les conditions pour sont assez rares. Il faut qu'en chargeant un paquet de 6 instructions, il y ait exactement trois instructions entières, une flottante, un branchement et un accès mémoire. Si le paquet contient 5 instructions entières et un branchement, seules quatre instructions sont émises. Dans cet exemple, dans la grande majorité des cas, seuls 4 ports seront utilisés en moyenne et deux seront sous-utilisés. Il s'agit là d'une moyenne, qui dépend du code, et des instructions exactes à exécuter. Cependant, cette moyenne nous donne un moyen d'économiser beaucoup de circuits pour un cout en performance mineur. L'idée est de ne charger que des blocs de 4 instructions, mais d'avoir 6 ports d'émission. Le processeur charge et décode 4 instructions, les quatre instructions sont envoyées sur les ports d'émission adéquat. Le cout en transistors est alors situé entre celui d'un CPU quadruple émission et sextuple émission. Et les performances sont aussi entre les deux. Pour les performances, c'est parce que cela permet d'avoir un CPU quadruple émission avec bien moins de contraintes d’appariement. Imaginez qu'on se soit limité à quatre ports d'émission : il aurait fallu mettre plusieurs unités sur le même port d'émission. En conséquence, il y aurait eu des combinaisons interdites, comme émettre deux instructions entières + un branchement + un accès mémoire ; ou deux instructions entières + une opération flottante + un accès mémoire. De tells appariement interdits sont possibles en utilisant les six ports d'émission. ==L'émission parallèle des accès mémoire== 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 simultanné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 à doubvle émission entière-flottante. [[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]] 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. Ce compromis change cependant quand on passe à la quadruple émission. ===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 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. Par contre, c'est autre chose sur les processeurs superscalaires larges. Vu qu'ils travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares. L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. La ''load/store queue'' reste une structure unique, mais on peut lui rajouter des ports. Et à ce peit jeu, il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire. [[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]] 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. Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre. Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Le Pentium 4 était dans ce cas, mais il était un peu seul dans sa catégorie. Un cas similaire est celui où l'unité mémoire est alimentée par une fenêtre de micro-opération mélangeant instructions mémoire et entières. Les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. Dans ce cas, l'émission multiple est rarement implémentée. Le cas suivant a une file pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique. Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui pouvaient seulement émettre une lecture en même temps qu'une écriture. Ils avaient deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse. [[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]] Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture demande d'utiliser une file d'écriture multiport, ce qui est assez compliqué. Implémenter des lectures en plus est plus simple : il suffit de rajouter une seconde file de µops LOAD et une unité de calcul, alimentées via un second port d'émission. Il est aussi possible de rajouter un second port de lecture au cache, mais ce n'est pas obligatoire. Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse. De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports de lecture et d'écriture car il y a plus de lecture que d'écritures. Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. : Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse. [[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]] Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettent plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail. ==Les optimisations des processeurs superscalaires larges== Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul. ===L'étape de chargement d'un CPU superscalaire large=== Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 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 autoaligné.]] Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements ! [[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]] Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez 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 microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total). Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois 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 microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement. Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations. ===La micro-fusion des processeurs Intel et AMD=== La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''. Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''. Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout. La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Le parallélisme mémoire | prevText=Le parallélisme mémoire | next=Exemples de microarchitectures CPU : le cas du x86 | nextText=Exemples de microarchitectures CPU : le cas du x86 }} </noinclude> aodl7ejqa949z8cc30s2ca0eukm903l 772727 772726 2026-09-21T18:59:22Z Mewtow 31375 /* L'émission multiple des micro-opérations entières */ 772727 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. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps. 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. 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. Deux branchements consécutifs sont quant à eux plus fréquents, mais exécuter deux branchements en même temps pose un problème : que faire quand les deux sont pris ? Deux accès mémoire consécutifs est assez rare. La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs sont systématiquement supportées, car dupliquer des ALU 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 et 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'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. Par exemple, l'unité de chargement charge deux fois plus d'instructions, mais n'est pas modifiée en profondeur. L'unité d'émission et les circuits d'exécution dans le désordre sont eux aussi altérés. Les unités de calcul sont souvent dupliquées, sauf sur les tout premiers CPU superscalaires. {|class="wikitable" |- ! rowspan="2" | Processeur sans émission multiple | rowspan="2" | Chargement | rowspan="2" | Décodage | rowspan="2" | Renommage | rowspan="2" | Émission | Exécution / ALU | rowspan="2" | ''Commit''/ROB |- | Exécution / ALU |- ! rowspan="4" | Processeur superscalaire | rowspan="4" | Chargement | rowspan="2" | Décodage | rowspan="4" | Renommage | rowspan="4" | Émission | Exécution / ALU | rowspan="4" | ''Commit''/ROB |- | Exécution / ALU |- | rowspan="2" | Décodage | Exécution / ALU |- | Exécution / ALU |} Voyons ce qu'il en est dans le détail. ===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. 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. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant. 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. ===Le chargement de plusieurs instructions consécutives=== Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large. Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence. Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas. Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement non-pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc. [[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]] ===Le décodage parallèle des instructions=== Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits. Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur. Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! ===L'unité d'émission d'un processeur superscalaire=== La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. 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 4 instructions, compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément deux 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. 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. : 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 microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle. 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=== Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement. 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. ==L'évolution historique des processeurs superscalaires== Les tout premiers processeurs superscalaires avaient un budget en transistors très limité, ce qui fait que les contraintes d'appariement étaient très strictes. Et au-delà de ça, ils ne faisaient que mieux utiliser des unités de calcul déjà présentes, ils ne dupliquaient aucune unité de calcul. Prenons un processeur non-superscalaire simple, illustré ci-dessous. Il contient : une unité de chargement, un décodeur, une unité d'émission, une ALU, une FPU, les bancs de registre entier et flottants, et une unité mémoire. Nous avons placé l'unité mémoire à la suite de l'ALU entière, car ce sera important pour les explications qui vont suivre. [[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]] Les processeurs superscalaires historiques modifiaient assez peu le schéma précédent. L'unité de chargement était adaptée pour lire plusieurs instructions depuis la mémoire, il y avait un plusieurs décodeurs, l'unité d'émission était un peu modifiée. Mais aucune unité de calcul n'était dupliquée. A la rigueur, gérer la triple émission avec les branchements demandait parfois d'ajouter des ports de lecture sur le banc de registre généraux, pas plus. L'évolution des CPU superscalaires historiques s'est faite en quatre temps, avec des design de départ simples, qui ont progressivement évolués. Les étapes portent les noms de : double émission entière-flottante, d'exécution parallèle des branchements, d'émission multiple des opérations entières. Après la quatrième étape, les CPU superscalaires sont devenus très variés, au point de ne pas pouvoir être classés simplement. ===La double émission entière-flottante=== Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est d'émettre une micro-opération entière en même temps qu'une micro-opé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, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité. La plupart des premiers processeurs superscalaires étaient de ce type. Les exemples les plus notables sont les processeurs POWER 1 et ses dérivés comme le ''RISC Single Chip''. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle. [[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]] La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisations visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate]. ===L'émission parallèle des branchements=== Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur. 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 modifier le banc de registres. Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. [[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]] Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications et les divisions ? Pour étudier ces questions, nous allons prendre le cas d'un processeur capable d’émettre trois opérations en même temps : deux opérations entières, et une flottante, potentiellement d'autres. {|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 |} 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|Emission multiple des opérations entières, implémentation courante]] Pour les branchements, deux solutions sont possibles. Avec la première, il y a émission parallèle des branchements, à savoir qu'il est possible d'émettre en branchement en parallèle de deux-trois instructions entières. Et cela impose la présence d'une unité de branchement séparée, avec son port d'émission. Avec la seconde, les branchements sont traités comme des instructions entières. Soit les branchements sont exécutés dans une ALU entière, soit il y a une unité de branchement reliée à un port d'émission pour les opérations entières. [[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Emission série et parallèle des branchements]] En général, les branchements sont exécutés comme des opérations entières. Cela permet d'économiser une unité de calcul et beaucoup de transistors dans l'unité d'émission/chargement/... {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 |- | * ALU entière * Multiplieur | * ALU entière * Barrel shifter * Unité de branchement | FPU |- | * Additions, soustractions, opérations bit à bit, etc. * Multiplication | * Additions, soustractions, opérations bit à bit, etc. * Décalages et rotations * Branchements |Opération flottante |} ===Les CPU superscalaires à plusieurs unités spécialisées=== L'évolution des CPU superscalaires s'est faite en quatre temps, et nous en sommes à la quatrième étape. Et cette quatrième étape ne correspond en réalité pas à grand chose de concret, ni à une évolution technique bien précise. Elle correspond en réalité à l'arrivée de processeur superscalaires très variés, qui n'ont pas grand chose en commun. Le premier CPU superscalaires utilisés dans les PC étaient le Pentium. Et c'était un CPU double émission un peu particulier. Il n'implémentait pas la double émission entière-flottante, mais il avait deux ALU entières. En clair, rien à voir avec une sorte d'évolution des designs précédents. Et il n'est pas le seul, la majorité des CPU superscalaire qui ont suivi ont privilégié l'ajout d'ALU entières. La raison est que les programmes informatiques utilisent beaucoup plus d'additions/soustractions que le reste. Il y a environ 5 opérations entières de base pour une multiplication entière, le ratio est le même quand on remplace la multiplication par un branchement ou un accès mémoire. En conséquence, les ALU entières ont été dupliquées en premier, et dans de nombreux exemplaires. Reste à voir comment a été dupliqué le reste. Les multiplieurs n'ont pas été dupliqués, sauf sur les processeurs superscalaires très larges. La raison est qu'il est rare de devoir exécuter deux multiplications à la fois. Et c'est la même chose pour le ''barrel shifter''. Deux branchements consécutifs sont quant à eux plus fréquents, c'est même assez courant. Aussi, les unités de branchement ont été dupliquées en second lieu, après les ALU entières. Avoir plusieurs accès mémoire proches est assez courant, surtout sur les CPU de type CISC. Aussi, les unités mémoire ont été dupliquées en troisième lieu. Pour résumer, dupliquer des ALU entières est donc la priorité, suivi par la duplication des unités de branchement, puis celle des accès mémoire, pour terminer par le multiplieur et le ''barrel shifter''. La FPU est un peu à part. La '''double émission flottante''' a été utilisée sur les processeurs Alpha. L'idée est d'émettre deux opérations flottantes à la fois. Sauf qu'une implémentation naïve demanderait de dupliquer la FPU, ce qui aurait un cout en circuit rédhibitoire. Pour rappel, les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant. L'idée est d'avoir des ports d'émission séparés pour l'additionneur flottant et le multiplieur flottant. Le processeur peut alors émettre une addition flottante en même temps qu'une multiplication flottante. Les autres circuits de calcul flottant sont répartis sur ces deux ports d'émission flottants. L'avantage est que cela se marie bien avec l'usage d'une fenêtre d'instruction séparée pour les opérations flottantes. La fenêtre d'instruction a alors deux ports séparés, au lieu d'un seul. Rajouter un second port d'émission flottant n'est pas trop un problème, car le cout lié à l'ajout d'un port n'est pas linéaire. Passer de un port à deux a un cout tolérable, bien plus que de passer de 3 ports à 4 ou de 4 à 5. Il est possible de dupliquer l'additionneur et le multiplieur flottant, ce qui permet d'émettre deux additions et multiplications flottantes en même temps. C'est ce qui est fait sur les processeurs AMD d'architecture Zen 1 et 2. Ils ont deux additionneurs flottants par cœur, deux multiplieurs flottants, chacun avec leur propre port d'émission. Les performances en calcul flottant sont assez impressionnantes pour un processeur de l'époque. [[File:ZEN - émission multiple flottante.png|centre|vignette|upright=2|Microarchitecture Zen 1 d'AMD.]] ==Les ports d'émission et de décodage== Les ports d'émission et de décodage sont des ressources matérielles comme les autres. Dans cette section, nous allons parler de concepts liés aux ports d'émission et de décodage. Nous allons notamment parler du partage des ports d'émission, ainsi que des relations numériques entre ces deux types de ports. ===Le partage des ports d'émission et la ''port contention''=== Pour le moment, nous n'avons vu que des cas où un port d'émission est relié soit à des ALU entières, soit à des FPU, pas aux deux. Mais il existe des processeurs où un même port d'émission alimente à la fois une ALU entière et une FPU. Par exemple, on peut relier un additionneur flottant sur le même port qu'une ALU entière. Il faut noter que cela implique une fenêtre d'instruction centralisée, capable de mettre en attente micro-opérations entières et flottantes. Un exemple est celui des processeurs Core 2 Duo. Ils disposent de 6 ports d'émission, dont 3 ports dédiés à l'unité mémoire. Les 3 ports restants alimentent chacun : une ALU entière, un circuit de calcul flottant et une unité de calcul SSE (une unité de calcul SIMD, qu'on abordera dans quelques chapitres). * Le premier port alimente une ALU entière simple et un multiplieur/diviseur flottant. * Le second alimente une ALU entière, un multiplieur entier et un additionneur flottant. * Le troisième alimente une ALU entière, sans circuit flottant dédié. Dans le même genre, il est possible de partager un port d'émission entre l'unité mémoire et une ALU entière. Cela permet d'utiliser l'ALU entière pour les calculs d'adresse, ce qui évite d'avoir à utiliser une unité de calcul d'adresse distincte. Un exemple est celui du processeur superscalaire double émission Power PC 440. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. L'organisation en question permet soit d'émettre un accès mémoire en même temps qu'une opération entière, soit d'émettre deux opérations entières simples, soit d’émettre une multiplication et une addition/soustraction/comparaison. Une organisation simple, mais très efficace ! [[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]] ===La séparation entre largeur de décodage et d'émission=== Sur les processeurs superscalaires larges, le processeur peut émettre plus d'instructions qu'il peut en décoder. Plus haut, j'ai dit que l'unité d'émission avait parfois plus de ports d'émission que de ports de décodage. La raison est assez simple, et elle s'explique assez bien avec un exemple. Prenons un processeur avec trois ALU entières, une FPU, une unité de branchement et une unité mémoire. Chacune a son propre port d'émission, ce qui fait que le CPU est capable d'émettre 6 µops. Du moins, c'est la théorie, car les conditions pour sont assez rares. Il faut qu'en chargeant un paquet de 6 instructions, il y ait exactement trois instructions entières, une flottante, un branchement et un accès mémoire. Si le paquet contient 5 instructions entières et un branchement, seules quatre instructions sont émises. Dans cet exemple, dans la grande majorité des cas, seuls 4 ports seront utilisés en moyenne et deux seront sous-utilisés. Il s'agit là d'une moyenne, qui dépend du code, et des instructions exactes à exécuter. Cependant, cette moyenne nous donne un moyen d'économiser beaucoup de circuits pour un cout en performance mineur. L'idée est de ne charger que des blocs de 4 instructions, mais d'avoir 6 ports d'émission. Le processeur charge et décode 4 instructions, les quatre instructions sont envoyées sur les ports d'émission adéquat. Le cout en transistors est alors situé entre celui d'un CPU quadruple émission et sextuple émission. Et les performances sont aussi entre les deux. Pour les performances, c'est parce que cela permet d'avoir un CPU quadruple émission avec bien moins de contraintes d’appariement. Imaginez qu'on se soit limité à quatre ports d'émission : il aurait fallu mettre plusieurs unités sur le même port d'émission. En conséquence, il y aurait eu des combinaisons interdites, comme émettre deux instructions entières + un branchement + un accès mémoire ; ou deux instructions entières + une opération flottante + un accès mémoire. De tells appariement interdits sont possibles en utilisant les six ports d'émission. ==L'émission parallèle des accès mémoire== 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 simultanné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 à doubvle émission entière-flottante. [[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]] 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. Ce compromis change cependant quand on passe à la quadruple émission. ===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 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. Par contre, c'est autre chose sur les processeurs superscalaires larges. Vu qu'ils travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares. L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. La ''load/store queue'' reste une structure unique, mais on peut lui rajouter des ports. Et à ce peit jeu, il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire. [[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]] 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. Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre. Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Le Pentium 4 était dans ce cas, mais il était un peu seul dans sa catégorie. Un cas similaire est celui où l'unité mémoire est alimentée par une fenêtre de micro-opération mélangeant instructions mémoire et entières. Les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. Dans ce cas, l'émission multiple est rarement implémentée. Le cas suivant a une file pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique. Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui pouvaient seulement émettre une lecture en même temps qu'une écriture. Ils avaient deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse. [[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]] Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture demande d'utiliser une file d'écriture multiport, ce qui est assez compliqué. Implémenter des lectures en plus est plus simple : il suffit de rajouter une seconde file de µops LOAD et une unité de calcul, alimentées via un second port d'émission. Il est aussi possible de rajouter un second port de lecture au cache, mais ce n'est pas obligatoire. Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse. De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports de lecture et d'écriture car il y a plus de lecture que d'écritures. Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. : Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse. [[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]] Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettent plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail. ==Les optimisations des processeurs superscalaires larges== Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul. ===L'étape de chargement d'un CPU superscalaire large=== Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 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 autoaligné.]] Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements ! [[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]] Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez 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 microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total). Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois 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 microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement. Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations. ===La micro-fusion des processeurs Intel et AMD=== La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''. Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''. Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout. La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Le parallélisme mémoire | prevText=Le parallélisme mémoire | next=Exemples de microarchitectures CPU : le cas du x86 | nextText=Exemples de microarchitectures CPU : le cas du x86 }} </noinclude> 4xxrf6qy4n31qaxay4kztfgd31x547w 772728 772727 2026-09-21T19:00:07Z Mewtow 31375 /* La séparation entre largeur de décodage et d'émission */ 772728 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. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps. 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. 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. Deux branchements consécutifs sont quant à eux plus fréquents, mais exécuter deux branchements en même temps pose un problème : que faire quand les deux sont pris ? Deux accès mémoire consécutifs est assez rare. La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs sont systématiquement supportées, car dupliquer des ALU 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 et 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'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. Par exemple, l'unité de chargement charge deux fois plus d'instructions, mais n'est pas modifiée en profondeur. L'unité d'émission et les circuits d'exécution dans le désordre sont eux aussi altérés. Les unités de calcul sont souvent dupliquées, sauf sur les tout premiers CPU superscalaires. {|class="wikitable" |- ! rowspan="2" | Processeur sans émission multiple | rowspan="2" | Chargement | rowspan="2" | Décodage | rowspan="2" | Renommage | rowspan="2" | Émission | Exécution / ALU | rowspan="2" | ''Commit''/ROB |- | Exécution / ALU |- ! rowspan="4" | Processeur superscalaire | rowspan="4" | Chargement | rowspan="2" | Décodage | rowspan="4" | Renommage | rowspan="4" | Émission | Exécution / ALU | rowspan="4" | ''Commit''/ROB |- | Exécution / ALU |- | rowspan="2" | Décodage | Exécution / ALU |- | Exécution / ALU |} Voyons ce qu'il en est dans le détail. ===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. 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. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant. 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. ===Le chargement de plusieurs instructions consécutives=== Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large. Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence. Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas. Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement non-pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc. [[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]] ===Le décodage parallèle des instructions=== Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits. Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur. Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! ===L'unité d'émission d'un processeur superscalaire=== La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. 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 4 instructions, compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément deux 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. 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. : 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 microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle. 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=== Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement. 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. ==L'évolution historique des processeurs superscalaires== Les tout premiers processeurs superscalaires avaient un budget en transistors très limité, ce qui fait que les contraintes d'appariement étaient très strictes. Et au-delà de ça, ils ne faisaient que mieux utiliser des unités de calcul déjà présentes, ils ne dupliquaient aucune unité de calcul. Prenons un processeur non-superscalaire simple, illustré ci-dessous. Il contient : une unité de chargement, un décodeur, une unité d'émission, une ALU, une FPU, les bancs de registre entier et flottants, et une unité mémoire. Nous avons placé l'unité mémoire à la suite de l'ALU entière, car ce sera important pour les explications qui vont suivre. [[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]] Les processeurs superscalaires historiques modifiaient assez peu le schéma précédent. L'unité de chargement était adaptée pour lire plusieurs instructions depuis la mémoire, il y avait un plusieurs décodeurs, l'unité d'émission était un peu modifiée. Mais aucune unité de calcul n'était dupliquée. A la rigueur, gérer la triple émission avec les branchements demandait parfois d'ajouter des ports de lecture sur le banc de registre généraux, pas plus. L'évolution des CPU superscalaires historiques s'est faite en quatre temps, avec des design de départ simples, qui ont progressivement évolués. Les étapes portent les noms de : double émission entière-flottante, d'exécution parallèle des branchements, d'émission multiple des opérations entières. Après la quatrième étape, les CPU superscalaires sont devenus très variés, au point de ne pas pouvoir être classés simplement. ===La double émission entière-flottante=== Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est d'émettre une micro-opération entière en même temps qu'une micro-opé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, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité. La plupart des premiers processeurs superscalaires étaient de ce type. Les exemples les plus notables sont les processeurs POWER 1 et ses dérivés comme le ''RISC Single Chip''. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle. [[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]] La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisations visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate]. ===L'émission parallèle des branchements=== Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur. 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 modifier le banc de registres. Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. [[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]] Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications et les divisions ? Pour étudier ces questions, nous allons prendre le cas d'un processeur capable d’émettre trois opérations en même temps : deux opérations entières, et une flottante, potentiellement d'autres. {|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 |} 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|Emission multiple des opérations entières, implémentation courante]] Pour les branchements, deux solutions sont possibles. Avec la première, il y a émission parallèle des branchements, à savoir qu'il est possible d'émettre en branchement en parallèle de deux-trois instructions entières. Et cela impose la présence d'une unité de branchement séparée, avec son port d'émission. Avec la seconde, les branchements sont traités comme des instructions entières. Soit les branchements sont exécutés dans une ALU entière, soit il y a une unité de branchement reliée à un port d'émission pour les opérations entières. [[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Emission série et parallèle des branchements]] En général, les branchements sont exécutés comme des opérations entières. Cela permet d'économiser une unité de calcul et beaucoup de transistors dans l'unité d'émission/chargement/... {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 |- | * ALU entière * Multiplieur | * ALU entière * Barrel shifter * Unité de branchement | FPU |- | * Additions, soustractions, opérations bit à bit, etc. * Multiplication | * Additions, soustractions, opérations bit à bit, etc. * Décalages et rotations * Branchements |Opération flottante |} ===Les CPU superscalaires à plusieurs unités spécialisées=== L'évolution des CPU superscalaires s'est faite en quatre temps, et nous en sommes à la quatrième étape. Et cette quatrième étape ne correspond en réalité pas à grand chose de concret, ni à une évolution technique bien précise. Elle correspond en réalité à l'arrivée de processeur superscalaires très variés, qui n'ont pas grand chose en commun. Le premier CPU superscalaires utilisés dans les PC étaient le Pentium. Et c'était un CPU double émission un peu particulier. Il n'implémentait pas la double émission entière-flottante, mais il avait deux ALU entières. En clair, rien à voir avec une sorte d'évolution des designs précédents. Et il n'est pas le seul, la majorité des CPU superscalaire qui ont suivi ont privilégié l'ajout d'ALU entières. La raison est que les programmes informatiques utilisent beaucoup plus d'additions/soustractions que le reste. Il y a environ 5 opérations entières de base pour une multiplication entière, le ratio est le même quand on remplace la multiplication par un branchement ou un accès mémoire. En conséquence, les ALU entières ont été dupliquées en premier, et dans de nombreux exemplaires. Reste à voir comment a été dupliqué le reste. Les multiplieurs n'ont pas été dupliqués, sauf sur les processeurs superscalaires très larges. La raison est qu'il est rare de devoir exécuter deux multiplications à la fois. Et c'est la même chose pour le ''barrel shifter''. Deux branchements consécutifs sont quant à eux plus fréquents, c'est même assez courant. Aussi, les unités de branchement ont été dupliquées en second lieu, après les ALU entières. Avoir plusieurs accès mémoire proches est assez courant, surtout sur les CPU de type CISC. Aussi, les unités mémoire ont été dupliquées en troisième lieu. Pour résumer, dupliquer des ALU entières est donc la priorité, suivi par la duplication des unités de branchement, puis celle des accès mémoire, pour terminer par le multiplieur et le ''barrel shifter''. La FPU est un peu à part. La '''double émission flottante''' a été utilisée sur les processeurs Alpha. L'idée est d'émettre deux opérations flottantes à la fois. Sauf qu'une implémentation naïve demanderait de dupliquer la FPU, ce qui aurait un cout en circuit rédhibitoire. Pour rappel, les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant. L'idée est d'avoir des ports d'émission séparés pour l'additionneur flottant et le multiplieur flottant. Le processeur peut alors émettre une addition flottante en même temps qu'une multiplication flottante. Les autres circuits de calcul flottant sont répartis sur ces deux ports d'émission flottants. L'avantage est que cela se marie bien avec l'usage d'une fenêtre d'instruction séparée pour les opérations flottantes. La fenêtre d'instruction a alors deux ports séparés, au lieu d'un seul. Rajouter un second port d'émission flottant n'est pas trop un problème, car le cout lié à l'ajout d'un port n'est pas linéaire. Passer de un port à deux a un cout tolérable, bien plus que de passer de 3 ports à 4 ou de 4 à 5. Il est possible de dupliquer l'additionneur et le multiplieur flottant, ce qui permet d'émettre deux additions et multiplications flottantes en même temps. C'est ce qui est fait sur les processeurs AMD d'architecture Zen 1 et 2. Ils ont deux additionneurs flottants par cœur, deux multiplieurs flottants, chacun avec leur propre port d'émission. Les performances en calcul flottant sont assez impressionnantes pour un processeur de l'époque. [[File:ZEN - émission multiple flottante.png|centre|vignette|upright=2|Microarchitecture Zen 1 d'AMD.]] ==Les ports d'émission et de décodage== Les ports d'émission et de décodage sont des ressources matérielles comme les autres. Dans cette section, nous allons parler de concepts liés aux ports d'émission et de décodage. Nous allons notamment parler du partage des ports d'émission, ainsi que des relations numériques entre ces deux types de ports. ===Le partage des ports d'émission et la ''port contention''=== Pour le moment, nous n'avons vu que des cas où un port d'émission est relié soit à des ALU entières, soit à des FPU, pas aux deux. Mais il existe des processeurs où un même port d'émission alimente à la fois une ALU entière et une FPU. Par exemple, on peut relier un additionneur flottant sur le même port qu'une ALU entière. Il faut noter que cela implique une fenêtre d'instruction centralisée, capable de mettre en attente micro-opérations entières et flottantes. Un exemple est celui des processeurs Core 2 Duo. Ils disposent de 6 ports d'émission, dont 3 ports dédiés à l'unité mémoire. Les 3 ports restants alimentent chacun : une ALU entière, un circuit de calcul flottant et une unité de calcul SSE (une unité de calcul SIMD, qu'on abordera dans quelques chapitres). * Le premier port alimente une ALU entière simple et un multiplieur/diviseur flottant. * Le second alimente une ALU entière, un multiplieur entier et un additionneur flottant. * Le troisième alimente une ALU entière, sans circuit flottant dédié. Dans le même genre, il est possible de partager un port d'émission entre l'unité mémoire et une ALU entière. Cela permet d'utiliser l'ALU entière pour les calculs d'adresse, ce qui évite d'avoir à utiliser une unité de calcul d'adresse distincte. Un exemple est celui du processeur superscalaire double émission Power PC 440. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. L'organisation en question permet soit d'émettre un accès mémoire en même temps qu'une opération entière, soit d'émettre deux opérations entières simples, soit d’émettre une multiplication et une addition/soustraction/comparaison. Une organisation simple, mais très efficace ! [[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]] ==L'émission parallèle des accès mémoire== 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 simultanné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 à doubvle émission entière-flottante. [[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]] 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. Ce compromis change cependant quand on passe à la quadruple émission. ===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 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. Par contre, c'est autre chose sur les processeurs superscalaires larges. Vu qu'ils travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares. L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. La ''load/store queue'' reste une structure unique, mais on peut lui rajouter des ports. Et à ce peit jeu, il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire. [[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]] 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. Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre. Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Le Pentium 4 était dans ce cas, mais il était un peu seul dans sa catégorie. Un cas similaire est celui où l'unité mémoire est alimentée par une fenêtre de micro-opération mélangeant instructions mémoire et entières. Les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. Dans ce cas, l'émission multiple est rarement implémentée. Le cas suivant a une file pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique. Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui pouvaient seulement émettre une lecture en même temps qu'une écriture. Ils avaient deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse. [[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]] Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture demande d'utiliser une file d'écriture multiport, ce qui est assez compliqué. Implémenter des lectures en plus est plus simple : il suffit de rajouter une seconde file de µops LOAD et une unité de calcul, alimentées via un second port d'émission. Il est aussi possible de rajouter un second port de lecture au cache, mais ce n'est pas obligatoire. Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse. De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports de lecture et d'écriture car il y a plus de lecture que d'écritures. Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. : Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse. [[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]] Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettent plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail. ==Les optimisations des processeurs superscalaires larges== Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul. ===L'étape de chargement d'un CPU superscalaire large=== Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 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 autoaligné.]] Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements ! [[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]] Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez 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 microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total). Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois 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 microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement. Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations. ===La micro-fusion des processeurs Intel et AMD=== La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''. Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''. Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout. La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Le parallélisme mémoire | prevText=Le parallélisme mémoire | next=Exemples de microarchitectures CPU : le cas du x86 | nextText=Exemples de microarchitectures CPU : le cas du x86 }} </noinclude> rhbajai89xyfa06pnyuthyirr3p694z 772729 772728 2026-09-21T19:00:41Z Mewtow 31375 /* Les ports d'émission et de décodage */ 772729 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. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps. 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. 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. Deux branchements consécutifs sont quant à eux plus fréquents, mais exécuter deux branchements en même temps pose un problème : que faire quand les deux sont pris ? Deux accès mémoire consécutifs est assez rare. La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs sont systématiquement supportées, car dupliquer des ALU 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 et 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'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. Par exemple, l'unité de chargement charge deux fois plus d'instructions, mais n'est pas modifiée en profondeur. L'unité d'émission et les circuits d'exécution dans le désordre sont eux aussi altérés. Les unités de calcul sont souvent dupliquées, sauf sur les tout premiers CPU superscalaires. {|class="wikitable" |- ! rowspan="2" | Processeur sans émission multiple | rowspan="2" | Chargement | rowspan="2" | Décodage | rowspan="2" | Renommage | rowspan="2" | Émission | Exécution / ALU | rowspan="2" | ''Commit''/ROB |- | Exécution / ALU |- ! rowspan="4" | Processeur superscalaire | rowspan="4" | Chargement | rowspan="2" | Décodage | rowspan="4" | Renommage | rowspan="4" | Émission | Exécution / ALU | rowspan="4" | ''Commit''/ROB |- | Exécution / ALU |- | rowspan="2" | Décodage | Exécution / ALU |- | Exécution / ALU |} Voyons ce qu'il en est dans le détail. ===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. 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. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant. 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. ===Le chargement de plusieurs instructions consécutives=== Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large. Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence. Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas. Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement non-pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc. [[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]] ===Le décodage parallèle des instructions=== Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits. Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur. Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! ===L'unité d'émission d'un processeur superscalaire=== La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. 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 4 instructions, compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément deux 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. 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. : 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 microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle. 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=== Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement. 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. ==L'évolution historique des processeurs superscalaires== Les tout premiers processeurs superscalaires avaient un budget en transistors très limité, ce qui fait que les contraintes d'appariement étaient très strictes. Et au-delà de ça, ils ne faisaient que mieux utiliser des unités de calcul déjà présentes, ils ne dupliquaient aucune unité de calcul. Prenons un processeur non-superscalaire simple, illustré ci-dessous. Il contient : une unité de chargement, un décodeur, une unité d'émission, une ALU, une FPU, les bancs de registre entier et flottants, et une unité mémoire. Nous avons placé l'unité mémoire à la suite de l'ALU entière, car ce sera important pour les explications qui vont suivre. [[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]] Les processeurs superscalaires historiques modifiaient assez peu le schéma précédent. L'unité de chargement était adaptée pour lire plusieurs instructions depuis la mémoire, il y avait un plusieurs décodeurs, l'unité d'émission était un peu modifiée. Mais aucune unité de calcul n'était dupliquée. A la rigueur, gérer la triple émission avec les branchements demandait parfois d'ajouter des ports de lecture sur le banc de registre généraux, pas plus. L'évolution des CPU superscalaires historiques s'est faite en quatre temps, avec des design de départ simples, qui ont progressivement évolués. Les étapes portent les noms de : double émission entière-flottante, d'exécution parallèle des branchements, d'émission multiple des opérations entières. Après la quatrième étape, les CPU superscalaires sont devenus très variés, au point de ne pas pouvoir être classés simplement. ===La double émission entière-flottante=== Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est d'émettre une micro-opération entière en même temps qu'une micro-opé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, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité. La plupart des premiers processeurs superscalaires étaient de ce type. Les exemples les plus notables sont les processeurs POWER 1 et ses dérivés comme le ''RISC Single Chip''. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle. [[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]] La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisations visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate]. ===L'émission parallèle des branchements=== Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur. 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 modifier le banc de registres. Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. [[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]] Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications et les divisions ? Pour étudier ces questions, nous allons prendre le cas d'un processeur capable d’émettre trois opérations en même temps : deux opérations entières, et une flottante, potentiellement d'autres. {|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 |} 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|Emission multiple des opérations entières, implémentation courante]] Pour les branchements, deux solutions sont possibles. Avec la première, il y a émission parallèle des branchements, à savoir qu'il est possible d'émettre en branchement en parallèle de deux-trois instructions entières. Et cela impose la présence d'une unité de branchement séparée, avec son port d'émission. Avec la seconde, les branchements sont traités comme des instructions entières. Soit les branchements sont exécutés dans une ALU entière, soit il y a une unité de branchement reliée à un port d'émission pour les opérations entières. [[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Emission série et parallèle des branchements]] En général, les branchements sont exécutés comme des opérations entières. Cela permet d'économiser une unité de calcul et beaucoup de transistors dans l'unité d'émission/chargement/... {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 |- | * ALU entière * Multiplieur | * ALU entière * Barrel shifter * Unité de branchement | FPU |- | * Additions, soustractions, opérations bit à bit, etc. * Multiplication | * Additions, soustractions, opérations bit à bit, etc. * Décalages et rotations * Branchements |Opération flottante |} ===Les CPU superscalaires à plusieurs unités spécialisées=== L'évolution des CPU superscalaires s'est faite en quatre temps, et nous en sommes à la quatrième étape. Et cette quatrième étape ne correspond en réalité pas à grand chose de concret, ni à une évolution technique bien précise. Elle correspond en réalité à l'arrivée de processeur superscalaires très variés, qui n'ont pas grand chose en commun. Le premier CPU superscalaires utilisés dans les PC étaient le Pentium. Et c'était un CPU double émission un peu particulier. Il n'implémentait pas la double émission entière-flottante, mais il avait deux ALU entières. En clair, rien à voir avec une sorte d'évolution des designs précédents. Et il n'est pas le seul, la majorité des CPU superscalaire qui ont suivi ont privilégié l'ajout d'ALU entières. La raison est que les programmes informatiques utilisent beaucoup plus d'additions/soustractions que le reste. Il y a environ 5 opérations entières de base pour une multiplication entière, le ratio est le même quand on remplace la multiplication par un branchement ou un accès mémoire. En conséquence, les ALU entières ont été dupliquées en premier, et dans de nombreux exemplaires. Reste à voir comment a été dupliqué le reste. Les multiplieurs n'ont pas été dupliqués, sauf sur les processeurs superscalaires très larges. La raison est qu'il est rare de devoir exécuter deux multiplications à la fois. Et c'est la même chose pour le ''barrel shifter''. Deux branchements consécutifs sont quant à eux plus fréquents, c'est même assez courant. Aussi, les unités de branchement ont été dupliquées en second lieu, après les ALU entières. Avoir plusieurs accès mémoire proches est assez courant, surtout sur les CPU de type CISC. Aussi, les unités mémoire ont été dupliquées en troisième lieu. Pour résumer, dupliquer des ALU entières est donc la priorité, suivi par la duplication des unités de branchement, puis celle des accès mémoire, pour terminer par le multiplieur et le ''barrel shifter''. La FPU est un peu à part. La '''double émission flottante''' a été utilisée sur les processeurs Alpha. L'idée est d'émettre deux opérations flottantes à la fois. Sauf qu'une implémentation naïve demanderait de dupliquer la FPU, ce qui aurait un cout en circuit rédhibitoire. Pour rappel, les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant. L'idée est d'avoir des ports d'émission séparés pour l'additionneur flottant et le multiplieur flottant. Le processeur peut alors émettre une addition flottante en même temps qu'une multiplication flottante. Les autres circuits de calcul flottant sont répartis sur ces deux ports d'émission flottants. L'avantage est que cela se marie bien avec l'usage d'une fenêtre d'instruction séparée pour les opérations flottantes. La fenêtre d'instruction a alors deux ports séparés, au lieu d'un seul. Rajouter un second port d'émission flottant n'est pas trop un problème, car le cout lié à l'ajout d'un port n'est pas linéaire. Passer de un port à deux a un cout tolérable, bien plus que de passer de 3 ports à 4 ou de 4 à 5. Il est possible de dupliquer l'additionneur et le multiplieur flottant, ce qui permet d'émettre deux additions et multiplications flottantes en même temps. C'est ce qui est fait sur les processeurs AMD d'architecture Zen 1 et 2. Ils ont deux additionneurs flottants par cœur, deux multiplieurs flottants, chacun avec leur propre port d'émission. Les performances en calcul flottant sont assez impressionnantes pour un processeur de l'époque. [[File:ZEN - émission multiple flottante.png|centre|vignette|upright=2|Microarchitecture Zen 1 d'AMD.]] Pour le moment, nous n'avons vu que des cas où un port d'émission est relié soit à des ALU entières, soit à des FPU, pas aux deux. Mais il existe des processeurs où un même port d'émission alimente à la fois une ALU entière et une FPU. Par exemple, on peut relier un additionneur flottant sur le même port qu'une ALU entière. Il faut noter que cela implique une fenêtre d'instruction centralisée, capable de mettre en attente micro-opérations entières et flottantes. Un exemple est celui des processeurs Core 2 Duo. Ils disposent de 6 ports d'émission, dont 3 ports dédiés à l'unité mémoire. Les 3 ports restants alimentent chacun : une ALU entière, un circuit de calcul flottant et une unité de calcul SSE (une unité de calcul SIMD, qu'on abordera dans quelques chapitres). * Le premier port alimente une ALU entière simple et un multiplieur/diviseur flottant. * Le second alimente une ALU entière, un multiplieur entier et un additionneur flottant. * Le troisième alimente une ALU entière, sans circuit flottant dédié. Dans le même genre, il est possible de partager un port d'émission entre l'unité mémoire et une ALU entière. Cela permet d'utiliser l'ALU entière pour les calculs d'adresse, ce qui évite d'avoir à utiliser une unité de calcul d'adresse distincte. Un exemple est celui du processeur superscalaire double émission Power PC 440. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. L'organisation en question permet soit d'émettre un accès mémoire en même temps qu'une opération entière, soit d'émettre deux opérations entières simples, soit d’émettre une multiplication et une addition/soustraction/comparaison. Une organisation simple, mais très efficace ! [[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]] ==L'émission parallèle des accès mémoire== 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 simultanné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 à doubvle émission entière-flottante. [[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]] 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. Ce compromis change cependant quand on passe à la quadruple émission. ===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 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. Par contre, c'est autre chose sur les processeurs superscalaires larges. Vu qu'ils travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares. L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. La ''load/store queue'' reste une structure unique, mais on peut lui rajouter des ports. Et à ce peit jeu, il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire. [[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]] 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. Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre. Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Le Pentium 4 était dans ce cas, mais il était un peu seul dans sa catégorie. Un cas similaire est celui où l'unité mémoire est alimentée par une fenêtre de micro-opération mélangeant instructions mémoire et entières. Les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. Dans ce cas, l'émission multiple est rarement implémentée. Le cas suivant a une file pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique. Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui pouvaient seulement émettre une lecture en même temps qu'une écriture. Ils avaient deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse. [[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]] Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture demande d'utiliser une file d'écriture multiport, ce qui est assez compliqué. Implémenter des lectures en plus est plus simple : il suffit de rajouter une seconde file de µops LOAD et une unité de calcul, alimentées via un second port d'émission. Il est aussi possible de rajouter un second port de lecture au cache, mais ce n'est pas obligatoire. Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse. De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports de lecture et d'écriture car il y a plus de lecture que d'écritures. Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. : Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse. [[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]] Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettent plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail. ==Les optimisations des processeurs superscalaires larges== Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul. ===L'étape de chargement d'un CPU superscalaire large=== Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 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 autoaligné.]] Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements ! [[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]] Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez 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 microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total). Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois 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 microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement. Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations. ===La micro-fusion des processeurs Intel et AMD=== La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''. Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''. Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout. La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Le parallélisme mémoire | prevText=Le parallélisme mémoire | next=Exemples de microarchitectures CPU : le cas du x86 | nextText=Exemples de microarchitectures CPU : le cas du x86 }} </noinclude> b12sue47h4l0b223av0zdjmat6z1e7j 772730 772729 2026-09-21T19:01:01Z Mewtow 31375 /* L'émission parallèle des accès mémoire */ 772730 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. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps. 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. 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. Deux branchements consécutifs sont quant à eux plus fréquents, mais exécuter deux branchements en même temps pose un problème : que faire quand les deux sont pris ? Deux accès mémoire consécutifs est assez rare. La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs sont systématiquement supportées, car dupliquer des ALU 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 et 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'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. Par exemple, l'unité de chargement charge deux fois plus d'instructions, mais n'est pas modifiée en profondeur. L'unité d'émission et les circuits d'exécution dans le désordre sont eux aussi altérés. Les unités de calcul sont souvent dupliquées, sauf sur les tout premiers CPU superscalaires. {|class="wikitable" |- ! rowspan="2" | Processeur sans émission multiple | rowspan="2" | Chargement | rowspan="2" | Décodage | rowspan="2" | Renommage | rowspan="2" | Émission | Exécution / ALU | rowspan="2" | ''Commit''/ROB |- | Exécution / ALU |- ! rowspan="4" | Processeur superscalaire | rowspan="4" | Chargement | rowspan="2" | Décodage | rowspan="4" | Renommage | rowspan="4" | Émission | Exécution / ALU | rowspan="4" | ''Commit''/ROB |- | Exécution / ALU |- | rowspan="2" | Décodage | Exécution / ALU |- | Exécution / ALU |} Voyons ce qu'il en est dans le détail. ===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. 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. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant. 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. ===Le chargement de plusieurs instructions consécutives=== Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large. Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence. Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas. Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement non-pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc. [[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]] ===Le décodage parallèle des instructions=== Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits. Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur. Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! ===L'unité d'émission d'un processeur superscalaire=== La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. 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 4 instructions, compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément deux 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. 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. : 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 microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle. 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=== Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement. 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. ==L'évolution historique des processeurs superscalaires== Les tout premiers processeurs superscalaires avaient un budget en transistors très limité, ce qui fait que les contraintes d'appariement étaient très strictes. Et au-delà de ça, ils ne faisaient que mieux utiliser des unités de calcul déjà présentes, ils ne dupliquaient aucune unité de calcul. Prenons un processeur non-superscalaire simple, illustré ci-dessous. Il contient : une unité de chargement, un décodeur, une unité d'émission, une ALU, une FPU, les bancs de registre entier et flottants, et une unité mémoire. Nous avons placé l'unité mémoire à la suite de l'ALU entière, car ce sera important pour les explications qui vont suivre. [[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]] Les processeurs superscalaires historiques modifiaient assez peu le schéma précédent. L'unité de chargement était adaptée pour lire plusieurs instructions depuis la mémoire, il y avait un plusieurs décodeurs, l'unité d'émission était un peu modifiée. Mais aucune unité de calcul n'était dupliquée. A la rigueur, gérer la triple émission avec les branchements demandait parfois d'ajouter des ports de lecture sur le banc de registre généraux, pas plus. L'évolution des CPU superscalaires historiques s'est faite en quatre temps, avec des design de départ simples, qui ont progressivement évolués. Les étapes portent les noms de : double émission entière-flottante, d'exécution parallèle des branchements, d'émission multiple des opérations entières. Après la quatrième étape, les CPU superscalaires sont devenus très variés, au point de ne pas pouvoir être classés simplement. ===La double émission entière-flottante=== Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est d'émettre une micro-opération entière en même temps qu'une micro-opé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, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité. La plupart des premiers processeurs superscalaires étaient de ce type. Les exemples les plus notables sont les processeurs POWER 1 et ses dérivés comme le ''RISC Single Chip''. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle. [[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]] La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisations visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate]. ===L'émission parallèle des branchements=== Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur. 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 modifier le banc de registres. Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. [[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]] Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications et les divisions ? Pour étudier ces questions, nous allons prendre le cas d'un processeur capable d’émettre trois opérations en même temps : deux opérations entières, et une flottante, potentiellement d'autres. {|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 |} 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|Emission multiple des opérations entières, implémentation courante]] Pour les branchements, deux solutions sont possibles. Avec la première, il y a émission parallèle des branchements, à savoir qu'il est possible d'émettre en branchement en parallèle de deux-trois instructions entières. Et cela impose la présence d'une unité de branchement séparée, avec son port d'émission. Avec la seconde, les branchements sont traités comme des instructions entières. Soit les branchements sont exécutés dans une ALU entière, soit il y a une unité de branchement reliée à un port d'émission pour les opérations entières. [[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Emission série et parallèle des branchements]] En général, les branchements sont exécutés comme des opérations entières. Cela permet d'économiser une unité de calcul et beaucoup de transistors dans l'unité d'émission/chargement/... {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 |- | * ALU entière * Multiplieur | * ALU entière * Barrel shifter * Unité de branchement | FPU |- | * Additions, soustractions, opérations bit à bit, etc. * Multiplication | * Additions, soustractions, opérations bit à bit, etc. * Décalages et rotations * Branchements |Opération flottante |} ===Les CPU superscalaires à plusieurs unités spécialisées=== L'évolution des CPU superscalaires s'est faite en quatre temps, et nous en sommes à la quatrième étape. Et cette quatrième étape ne correspond en réalité pas à grand chose de concret, ni à une évolution technique bien précise. Elle correspond en réalité à l'arrivée de processeur superscalaires très variés, qui n'ont pas grand chose en commun. Le premier CPU superscalaires utilisés dans les PC étaient le Pentium. Et c'était un CPU double émission un peu particulier. Il n'implémentait pas la double émission entière-flottante, mais il avait deux ALU entières. En clair, rien à voir avec une sorte d'évolution des designs précédents. Et il n'est pas le seul, la majorité des CPU superscalaire qui ont suivi ont privilégié l'ajout d'ALU entières. La raison est que les programmes informatiques utilisent beaucoup plus d'additions/soustractions que le reste. Il y a environ 5 opérations entières de base pour une multiplication entière, le ratio est le même quand on remplace la multiplication par un branchement ou un accès mémoire. En conséquence, les ALU entières ont été dupliquées en premier, et dans de nombreux exemplaires. Reste à voir comment a été dupliqué le reste. Les multiplieurs n'ont pas été dupliqués, sauf sur les processeurs superscalaires très larges. La raison est qu'il est rare de devoir exécuter deux multiplications à la fois. Et c'est la même chose pour le ''barrel shifter''. Deux branchements consécutifs sont quant à eux plus fréquents, c'est même assez courant. Aussi, les unités de branchement ont été dupliquées en second lieu, après les ALU entières. Avoir plusieurs accès mémoire proches est assez courant, surtout sur les CPU de type CISC. Aussi, les unités mémoire ont été dupliquées en troisième lieu. Pour résumer, dupliquer des ALU entières est donc la priorité, suivi par la duplication des unités de branchement, puis celle des accès mémoire, pour terminer par le multiplieur et le ''barrel shifter''. La FPU est un peu à part. La '''double émission flottante''' a été utilisée sur les processeurs Alpha. L'idée est d'émettre deux opérations flottantes à la fois. Sauf qu'une implémentation naïve demanderait de dupliquer la FPU, ce qui aurait un cout en circuit rédhibitoire. Pour rappel, les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant. L'idée est d'avoir des ports d'émission séparés pour l'additionneur flottant et le multiplieur flottant. Le processeur peut alors émettre une addition flottante en même temps qu'une multiplication flottante. Les autres circuits de calcul flottant sont répartis sur ces deux ports d'émission flottants. L'avantage est que cela se marie bien avec l'usage d'une fenêtre d'instruction séparée pour les opérations flottantes. La fenêtre d'instruction a alors deux ports séparés, au lieu d'un seul. Rajouter un second port d'émission flottant n'est pas trop un problème, car le cout lié à l'ajout d'un port n'est pas linéaire. Passer de un port à deux a un cout tolérable, bien plus que de passer de 3 ports à 4 ou de 4 à 5. Il est possible de dupliquer l'additionneur et le multiplieur flottant, ce qui permet d'émettre deux additions et multiplications flottantes en même temps. C'est ce qui est fait sur les processeurs AMD d'architecture Zen 1 et 2. Ils ont deux additionneurs flottants par cœur, deux multiplieurs flottants, chacun avec leur propre port d'émission. Les performances en calcul flottant sont assez impressionnantes pour un processeur de l'époque. [[File:ZEN - émission multiple flottante.png|centre|vignette|upright=2|Microarchitecture Zen 1 d'AMD.]] Pour le moment, nous n'avons vu que des cas où un port d'émission est relié soit à des ALU entières, soit à des FPU, pas aux deux. Mais il existe des processeurs où un même port d'émission alimente à la fois une ALU entière et une FPU. Par exemple, on peut relier un additionneur flottant sur le même port qu'une ALU entière. Il faut noter que cela implique une fenêtre d'instruction centralisée, capable de mettre en attente micro-opérations entières et flottantes. Un exemple est celui des processeurs Core 2 Duo. Ils disposent de 6 ports d'émission, dont 3 ports dédiés à l'unité mémoire. Les 3 ports restants alimentent chacun : une ALU entière, un circuit de calcul flottant et une unité de calcul SSE (une unité de calcul SIMD, qu'on abordera dans quelques chapitres). * Le premier port alimente une ALU entière simple et un multiplieur/diviseur flottant. * Le second alimente une ALU entière, un multiplieur entier et un additionneur flottant. * Le troisième alimente une ALU entière, sans circuit flottant dédié. Dans le même genre, il est possible de partager un port d'émission entre l'unité mémoire et une ALU entière. Cela permet d'utiliser l'ALU entière pour les calculs d'adresse, ce qui évite d'avoir à utiliser une unité de calcul d'adresse distincte. Un exemple est celui du processeur superscalaire double émission Power PC 440. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. L'organisation en question permet soit d'émettre un accès mémoire en même temps qu'une opération entière, soit d'émettre deux opérations entières simples, soit d’émettre une multiplication et une addition/soustraction/comparaison. Une organisation simple, mais très efficace ! [[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]] ==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 simultanné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 à doubvle émission entière-flottante. [[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]] 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. Ce compromis change cependant quand on passe à la quadruple émission. ===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 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. Par contre, c'est autre chose sur les processeurs superscalaires larges. Vu qu'ils travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares. L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. La ''load/store queue'' reste une structure unique, mais on peut lui rajouter des ports. Et à ce peit jeu, il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire. [[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]] 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. Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre. Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Le Pentium 4 était dans ce cas, mais il était un peu seul dans sa catégorie. Un cas similaire est celui où l'unité mémoire est alimentée par une fenêtre de micro-opération mélangeant instructions mémoire et entières. Les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. Dans ce cas, l'émission multiple est rarement implémentée. Le cas suivant a une file pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique. Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui pouvaient seulement émettre une lecture en même temps qu'une écriture. Ils avaient deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse. [[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]] Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture demande d'utiliser une file d'écriture multiport, ce qui est assez compliqué. Implémenter des lectures en plus est plus simple : il suffit de rajouter une seconde file de µops LOAD et une unité de calcul, alimentées via un second port d'émission. Il est aussi possible de rajouter un second port de lecture au cache, mais ce n'est pas obligatoire. Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse. De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports de lecture et d'écriture car il y a plus de lecture que d'écritures. Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. : Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse. [[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]] Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettent plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail. ==Les optimisations des processeurs superscalaires larges== Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul. ===L'étape de chargement d'un CPU superscalaire large=== Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 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 autoaligné.]] Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements ! [[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]] Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez 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 microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total). Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois 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 microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement. Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations. ===La micro-fusion des processeurs Intel et AMD=== La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''. Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''. Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout. La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Le parallélisme mémoire | prevText=Le parallélisme mémoire | next=Exemples de microarchitectures CPU : le cas du x86 | nextText=Exemples de microarchitectures CPU : le cas du x86 }} </noinclude> ro5yp9yhcefx8e15fl9m2p4h0lzejem 772731 772730 2026-09-21T19:08:44Z Mewtow 31375 /* Les accès mémoire traités dans le pipeline entier */ 772731 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. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps. 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. 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. Deux branchements consécutifs sont quant à eux plus fréquents, mais exécuter deux branchements en même temps pose un problème : que faire quand les deux sont pris ? Deux accès mémoire consécutifs est assez rare. La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs sont systématiquement supportées, car dupliquer des ALU 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 et 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'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. Par exemple, l'unité de chargement charge deux fois plus d'instructions, mais n'est pas modifiée en profondeur. L'unité d'émission et les circuits d'exécution dans le désordre sont eux aussi altérés. Les unités de calcul sont souvent dupliquées, sauf sur les tout premiers CPU superscalaires. {|class="wikitable" |- ! rowspan="2" | Processeur sans émission multiple | rowspan="2" | Chargement | rowspan="2" | Décodage | rowspan="2" | Renommage | rowspan="2" | Émission | Exécution / ALU | rowspan="2" | ''Commit''/ROB |- | Exécution / ALU |- ! rowspan="4" | Processeur superscalaire | rowspan="4" | Chargement | rowspan="2" | Décodage | rowspan="4" | Renommage | rowspan="4" | Émission | Exécution / ALU | rowspan="4" | ''Commit''/ROB |- | Exécution / ALU |- | rowspan="2" | Décodage | Exécution / ALU |- | Exécution / ALU |} Voyons ce qu'il en est dans le détail. ===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. 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. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant. 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. ===Le chargement de plusieurs instructions consécutives=== Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large. Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence. Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas. Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement non-pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc. [[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]] ===Le décodage parallèle des instructions=== Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits. Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur. Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! ===L'unité d'émission d'un processeur superscalaire=== La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. 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 4 instructions, compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément deux 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. 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. : 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 microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle. 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=== Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement. 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. ==L'évolution historique des processeurs superscalaires== Les tout premiers processeurs superscalaires avaient un budget en transistors très limité, ce qui fait que les contraintes d'appariement étaient très strictes. Et au-delà de ça, ils ne faisaient que mieux utiliser des unités de calcul déjà présentes, ils ne dupliquaient aucune unité de calcul. Prenons un processeur non-superscalaire simple, illustré ci-dessous. Il contient : une unité de chargement, un décodeur, une unité d'émission, une ALU, une FPU, les bancs de registre entier et flottants, et une unité mémoire. Nous avons placé l'unité mémoire à la suite de l'ALU entière, car ce sera important pour les explications qui vont suivre. [[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]] Les processeurs superscalaires historiques modifiaient assez peu le schéma précédent. L'unité de chargement était adaptée pour lire plusieurs instructions depuis la mémoire, il y avait un plusieurs décodeurs, l'unité d'émission était un peu modifiée. Mais aucune unité de calcul n'était dupliquée. A la rigueur, gérer la triple émission avec les branchements demandait parfois d'ajouter des ports de lecture sur le banc de registre généraux, pas plus. L'évolution des CPU superscalaires historiques s'est faite en quatre temps, avec des design de départ simples, qui ont progressivement évolués. Les étapes portent les noms de : double émission entière-flottante, d'exécution parallèle des branchements, d'émission multiple des opérations entières. Après la quatrième étape, les CPU superscalaires sont devenus très variés, au point de ne pas pouvoir être classés simplement. ===La double émission entière-flottante=== Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est d'émettre une micro-opération entière en même temps qu'une micro-opé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, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité. La plupart des premiers processeurs superscalaires étaient de ce type. Les exemples les plus notables sont les processeurs POWER 1 et ses dérivés comme le ''RISC Single Chip''. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle. [[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]] La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisations visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate]. ===L'émission parallèle des branchements=== Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur. 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 modifier le banc de registres. Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. [[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]] Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications et les divisions ? Pour étudier ces questions, nous allons prendre le cas d'un processeur capable d’émettre trois opérations en même temps : deux opérations entières, et une flottante, potentiellement d'autres. {|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 |} 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|Emission multiple des opérations entières, implémentation courante]] Pour les branchements, deux solutions sont possibles. Avec la première, il y a émission parallèle des branchements, à savoir qu'il est possible d'émettre en branchement en parallèle de deux-trois instructions entières. Et cela impose la présence d'une unité de branchement séparée, avec son port d'émission. Avec la seconde, les branchements sont traités comme des instructions entières. Soit les branchements sont exécutés dans une ALU entière, soit il y a une unité de branchement reliée à un port d'émission pour les opérations entières. [[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Emission série et parallèle des branchements]] En général, les branchements sont exécutés comme des opérations entières. Cela permet d'économiser une unité de calcul et beaucoup de transistors dans l'unité d'émission/chargement/... {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 |- | * ALU entière * Multiplieur | * ALU entière * Barrel shifter * Unité de branchement | FPU |- | * Additions, soustractions, opérations bit à bit, etc. * Multiplication | * Additions, soustractions, opérations bit à bit, etc. * Décalages et rotations * Branchements |Opération flottante |} ===Les CPU superscalaires à plusieurs unités spécialisées=== L'évolution des CPU superscalaires s'est faite en quatre temps, et nous en sommes à la quatrième étape. Et cette quatrième étape ne correspond en réalité pas à grand chose de concret, ni à une évolution technique bien précise. Elle correspond en réalité à l'arrivée de processeur superscalaires très variés, qui n'ont pas grand chose en commun. Le premier CPU superscalaires utilisés dans les PC étaient le Pentium. Et c'était un CPU double émission un peu particulier. Il n'implémentait pas la double émission entière-flottante, mais il avait deux ALU entières. En clair, rien à voir avec une sorte d'évolution des designs précédents. Et il n'est pas le seul, la majorité des CPU superscalaire qui ont suivi ont privilégié l'ajout d'ALU entières. La raison est que les programmes informatiques utilisent beaucoup plus d'additions/soustractions que le reste. Il y a environ 5 opérations entières de base pour une multiplication entière, le ratio est le même quand on remplace la multiplication par un branchement ou un accès mémoire. En conséquence, les ALU entières ont été dupliquées en premier, et dans de nombreux exemplaires. Reste à voir comment a été dupliqué le reste. Les multiplieurs n'ont pas été dupliqués, sauf sur les processeurs superscalaires très larges. La raison est qu'il est rare de devoir exécuter deux multiplications à la fois. Et c'est la même chose pour le ''barrel shifter''. Deux branchements consécutifs sont quant à eux plus fréquents, c'est même assez courant. Aussi, les unités de branchement ont été dupliquées en second lieu, après les ALU entières. Avoir plusieurs accès mémoire proches est assez courant, surtout sur les CPU de type CISC. Aussi, les unités mémoire ont été dupliquées en troisième lieu. Pour résumer, dupliquer des ALU entières est donc la priorité, suivi par la duplication des unités de branchement, puis celle des accès mémoire, pour terminer par le multiplieur et le ''barrel shifter''. La FPU est un peu à part. La '''double émission flottante''' a été utilisée sur les processeurs Alpha. L'idée est d'émettre deux opérations flottantes à la fois. Sauf qu'une implémentation naïve demanderait de dupliquer la FPU, ce qui aurait un cout en circuit rédhibitoire. Pour rappel, les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant. L'idée est d'avoir des ports d'émission séparés pour l'additionneur flottant et le multiplieur flottant. Le processeur peut alors émettre une addition flottante en même temps qu'une multiplication flottante. Les autres circuits de calcul flottant sont répartis sur ces deux ports d'émission flottants. L'avantage est que cela se marie bien avec l'usage d'une fenêtre d'instruction séparée pour les opérations flottantes. La fenêtre d'instruction a alors deux ports séparés, au lieu d'un seul. Rajouter un second port d'émission flottant n'est pas trop un problème, car le cout lié à l'ajout d'un port n'est pas linéaire. Passer de un port à deux a un cout tolérable, bien plus que de passer de 3 ports à 4 ou de 4 à 5. Il est possible de dupliquer l'additionneur et le multiplieur flottant, ce qui permet d'émettre deux additions et multiplications flottantes en même temps. C'est ce qui est fait sur les processeurs AMD d'architecture Zen 1 et 2. Ils ont deux additionneurs flottants par cœur, deux multiplieurs flottants, chacun avec leur propre port d'émission. Les performances en calcul flottant sont assez impressionnantes pour un processeur de l'époque. [[File:ZEN - émission multiple flottante.png|centre|vignette|upright=2|Microarchitecture Zen 1 d'AMD.]] Pour le moment, nous n'avons vu que des cas où un port d'émission est relié soit à des ALU entières, soit à des FPU, pas aux deux. Mais il existe des processeurs où un même port d'émission alimente à la fois une ALU entière et une FPU. Par exemple, on peut relier un additionneur flottant sur le même port qu'une ALU entière. Il faut noter que cela implique une fenêtre d'instruction centralisée, capable de mettre en attente micro-opérations entières et flottantes. Un exemple est celui des processeurs Core 2 Duo. Ils disposent de 6 ports d'émission, dont 3 ports dédiés à l'unité mémoire. Les 3 ports restants alimentent chacun : une ALU entière, un circuit de calcul flottant et une unité de calcul SSE (une unité de calcul SIMD, qu'on abordera dans quelques chapitres). * Le premier port alimente une ALU entière simple et un multiplieur/diviseur flottant. * Le second alimente une ALU entière, un multiplieur entier et un additionneur flottant. * Le troisième alimente une ALU entière, sans circuit flottant dédié. Dans le même genre, il est possible de partager un port d'émission entre l'unité mémoire et une ALU entière. Cela permet d'utiliser l'ALU entière pour les calculs d'adresse, ce qui évite d'avoir à utiliser une unité de calcul d'adresse distincte. Un exemple est celui du processeur superscalaire double émission Power PC 440. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. L'organisation en question permet soit d'émettre un accès mémoire en même temps qu'une opération entière, soit d'émettre deux opérations entières simples, soit d’émettre une multiplication et une addition/soustraction/comparaison. Une organisation simple, mais très efficace ! [[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]] ==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 simultanné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]] 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 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. Par contre, c'est autre chose sur les processeurs superscalaires larges. Vu qu'ils travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares. L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. La ''load/store queue'' reste une structure unique, mais on peut lui rajouter des ports. Et à ce peit jeu, il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire. [[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]] 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. Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre. Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Le Pentium 4 était dans ce cas, mais il était un peu seul dans sa catégorie. Un cas similaire est celui où l'unité mémoire est alimentée par une fenêtre de micro-opération mélangeant instructions mémoire et entières. Les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. Dans ce cas, l'émission multiple est rarement implémentée. Le cas suivant a une file pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique. Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui pouvaient seulement émettre une lecture en même temps qu'une écriture. Ils avaient deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse. [[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]] Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture demande d'utiliser une file d'écriture multiport, ce qui est assez compliqué. Implémenter des lectures en plus est plus simple : il suffit de rajouter une seconde file de µops LOAD et une unité de calcul, alimentées via un second port d'émission. Il est aussi possible de rajouter un second port de lecture au cache, mais ce n'est pas obligatoire. Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse. De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports de lecture et d'écriture car il y a plus de lecture que d'écritures. Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. : Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse. [[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]] Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettent plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail. ==Les optimisations des processeurs superscalaires larges== Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul. ===L'étape de chargement d'un CPU superscalaire large=== Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 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 autoaligné.]] Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements ! [[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]] Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez 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 microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total). Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois 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 microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement. Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations. ===La micro-fusion des processeurs Intel et AMD=== La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''. Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''. Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout. La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Le parallélisme mémoire | prevText=Le parallélisme mémoire | next=Exemples de microarchitectures CPU : le cas du x86 | nextText=Exemples de microarchitectures CPU : le cas du x86 }} </noinclude> ekr6afh7cmxsh55r8r2rgvjn51alsh5 772732 772731 2026-09-21T19:11:17Z Mewtow 31375 /* Le banc de registre d'un processeur superscalaire */ 772732 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. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps. 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. 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. Deux branchements consécutifs sont quant à eux plus fréquents, mais exécuter deux branchements en même temps pose un problème : que faire quand les deux sont pris ? Deux accès mémoire consécutifs est assez rare. La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs sont systématiquement supportées, car dupliquer des ALU 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 et 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'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. Par exemple, l'unité de chargement charge deux fois plus d'instructions, mais n'est pas modifiée en profondeur. L'unité d'émission et les circuits d'exécution dans le désordre sont eux aussi altérés. Les unités de calcul sont souvent dupliquées, sauf sur les tout premiers CPU superscalaires. {|class="wikitable" |- ! rowspan="2" | Processeur sans émission multiple | rowspan="2" | Chargement | rowspan="2" | Décodage | rowspan="2" | Renommage | rowspan="2" | Émission | Exécution / ALU | rowspan="2" | ''Commit''/ROB |- | Exécution / ALU |- ! rowspan="4" | Processeur superscalaire | rowspan="4" | Chargement | rowspan="2" | Décodage | rowspan="4" | Renommage | rowspan="4" | Émission | Exécution / ALU | rowspan="4" | ''Commit''/ROB |- | Exécution / ALU |- | rowspan="2" | Décodage | Exécution / ALU |- | Exécution / ALU |} Voyons ce qu'il en est dans le détail. ===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. 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. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant. 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. ===Le chargement de plusieurs instructions consécutives=== Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large. Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence. Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas. Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement non-pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc. [[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]] ===Le décodage parallèle des instructions=== Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits. Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur. Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! ===L'unité d'émission d'un processeur superscalaire=== La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. 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 4 instructions, compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément deux 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. 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. : 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 microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle. 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. ==L'évolution historique des processeurs superscalaires== Les tout premiers processeurs superscalaires avaient un budget en transistors très limité, ce qui fait que les contraintes d'appariement étaient très strictes. Et au-delà de ça, ils ne faisaient que mieux utiliser des unités de calcul déjà présentes, ils ne dupliquaient aucune unité de calcul. Prenons un processeur non-superscalaire simple, illustré ci-dessous. Il contient : une unité de chargement, un décodeur, une unité d'émission, une ALU, une FPU, les bancs de registre entier et flottants, et une unité mémoire. Nous avons placé l'unité mémoire à la suite de l'ALU entière, car ce sera important pour les explications qui vont suivre. [[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]] Les processeurs superscalaires historiques modifiaient assez peu le schéma précédent. L'unité de chargement était adaptée pour lire plusieurs instructions depuis la mémoire, il y avait un plusieurs décodeurs, l'unité d'émission était un peu modifiée. Mais aucune unité de calcul n'était dupliquée. A la rigueur, gérer la triple émission avec les branchements demandait parfois d'ajouter des ports de lecture sur le banc de registre généraux, pas plus. L'évolution des CPU superscalaires historiques s'est faite en quatre temps, avec des design de départ simples, qui ont progressivement évolués. Les étapes portent les noms de : double émission entière-flottante, d'exécution parallèle des branchements, d'émission multiple des opérations entières. Après la quatrième étape, les CPU superscalaires sont devenus très variés, au point de ne pas pouvoir être classés simplement. ===La double émission entière-flottante=== Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est d'émettre une micro-opération entière en même temps qu'une micro-opé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, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité. La plupart des premiers processeurs superscalaires étaient de ce type. Les exemples les plus notables sont les processeurs POWER 1 et ses dérivés comme le ''RISC Single Chip''. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle. [[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]] La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisations visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate]. ===L'émission parallèle des branchements=== Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur. 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 modifier le banc de registres. Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. [[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]] Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications et les divisions ? Pour étudier ces questions, nous allons prendre le cas d'un processeur capable d’émettre trois opérations en même temps : deux opérations entières, et une flottante, potentiellement d'autres. {|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 |} 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|Emission multiple des opérations entières, implémentation courante]] Pour les branchements, deux solutions sont possibles. Avec la première, il y a émission parallèle des branchements, à savoir qu'il est possible d'émettre en branchement en parallèle de deux-trois instructions entières. Et cela impose la présence d'une unité de branchement séparée, avec son port d'émission. Avec la seconde, les branchements sont traités comme des instructions entières. Soit les branchements sont exécutés dans une ALU entière, soit il y a une unité de branchement reliée à un port d'émission pour les opérations entières. [[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Emission série et parallèle des branchements]] En général, les branchements sont exécutés comme des opérations entières. Cela permet d'économiser une unité de calcul et beaucoup de transistors dans l'unité d'émission/chargement/... {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 |- | * ALU entière * Multiplieur | * ALU entière * Barrel shifter * Unité de branchement | FPU |- | * Additions, soustractions, opérations bit à bit, etc. * Multiplication | * Additions, soustractions, opérations bit à bit, etc. * Décalages et rotations * Branchements |Opération flottante |} ===Les CPU superscalaires à plusieurs unités spécialisées=== L'évolution des CPU superscalaires s'est faite en quatre temps, et nous en sommes à la quatrième étape. Et cette quatrième étape ne correspond en réalité pas à grand chose de concret, ni à une évolution technique bien précise. Elle correspond en réalité à l'arrivée de processeur superscalaires très variés, qui n'ont pas grand chose en commun. Le premier CPU superscalaires utilisés dans les PC étaient le Pentium. Et c'était un CPU double émission un peu particulier. Il n'implémentait pas la double émission entière-flottante, mais il avait deux ALU entières. En clair, rien à voir avec une sorte d'évolution des designs précédents. Et il n'est pas le seul, la majorité des CPU superscalaire qui ont suivi ont privilégié l'ajout d'ALU entières. La raison est que les programmes informatiques utilisent beaucoup plus d'additions/soustractions que le reste. Il y a environ 5 opérations entières de base pour une multiplication entière, le ratio est le même quand on remplace la multiplication par un branchement ou un accès mémoire. En conséquence, les ALU entières ont été dupliquées en premier, et dans de nombreux exemplaires. Reste à voir comment a été dupliqué le reste. Les multiplieurs n'ont pas été dupliqués, sauf sur les processeurs superscalaires très larges. La raison est qu'il est rare de devoir exécuter deux multiplications à la fois. Et c'est la même chose pour le ''barrel shifter''. Deux branchements consécutifs sont quant à eux plus fréquents, c'est même assez courant. Aussi, les unités de branchement ont été dupliquées en second lieu, après les ALU entières. Avoir plusieurs accès mémoire proches est assez courant, surtout sur les CPU de type CISC. Aussi, les unités mémoire ont été dupliquées en troisième lieu. Pour résumer, dupliquer des ALU entières est donc la priorité, suivi par la duplication des unités de branchement, puis celle des accès mémoire, pour terminer par le multiplieur et le ''barrel shifter''. La FPU est un peu à part. La '''double émission flottante''' a été utilisée sur les processeurs Alpha. L'idée est d'émettre deux opérations flottantes à la fois. Sauf qu'une implémentation naïve demanderait de dupliquer la FPU, ce qui aurait un cout en circuit rédhibitoire. Pour rappel, les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant. L'idée est d'avoir des ports d'émission séparés pour l'additionneur flottant et le multiplieur flottant. Le processeur peut alors émettre une addition flottante en même temps qu'une multiplication flottante. Les autres circuits de calcul flottant sont répartis sur ces deux ports d'émission flottants. L'avantage est que cela se marie bien avec l'usage d'une fenêtre d'instruction séparée pour les opérations flottantes. La fenêtre d'instruction a alors deux ports séparés, au lieu d'un seul. Rajouter un second port d'émission flottant n'est pas trop un problème, car le cout lié à l'ajout d'un port n'est pas linéaire. Passer de un port à deux a un cout tolérable, bien plus que de passer de 3 ports à 4 ou de 4 à 5. Il est possible de dupliquer l'additionneur et le multiplieur flottant, ce qui permet d'émettre deux additions et multiplications flottantes en même temps. C'est ce qui est fait sur les processeurs AMD d'architecture Zen 1 et 2. Ils ont deux additionneurs flottants par cœur, deux multiplieurs flottants, chacun avec leur propre port d'émission. Les performances en calcul flottant sont assez impressionnantes pour un processeur de l'époque. [[File:ZEN - émission multiple flottante.png|centre|vignette|upright=2|Microarchitecture Zen 1 d'AMD.]] Pour le moment, nous n'avons vu que des cas où un port d'émission est relié soit à des ALU entières, soit à des FPU, pas aux deux. Mais il existe des processeurs où un même port d'émission alimente à la fois une ALU entière et une FPU. Par exemple, on peut relier un additionneur flottant sur le même port qu'une ALU entière. Il faut noter que cela implique une fenêtre d'instruction centralisée, capable de mettre en attente micro-opérations entières et flottantes. Un exemple est celui des processeurs Core 2 Duo. Ils disposent de 6 ports d'émission, dont 3 ports dédiés à l'unité mémoire. Les 3 ports restants alimentent chacun : une ALU entière, un circuit de calcul flottant et une unité de calcul SSE (une unité de calcul SIMD, qu'on abordera dans quelques chapitres). * Le premier port alimente une ALU entière simple et un multiplieur/diviseur flottant. * Le second alimente une ALU entière, un multiplieur entier et un additionneur flottant. * Le troisième alimente une ALU entière, sans circuit flottant dédié. Dans le même genre, il est possible de partager un port d'émission entre l'unité mémoire et une ALU entière. Cela permet d'utiliser l'ALU entière pour les calculs d'adresse, ce qui évite d'avoir à utiliser une unité de calcul d'adresse distincte. Un exemple est celui du processeur superscalaire double émission Power PC 440. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. L'organisation en question permet soit d'émettre un accès mémoire en même temps qu'une opération entière, soit d'émettre deux opérations entières simples, soit d’émettre une multiplication et une addition/soustraction/comparaison. Une organisation simple, mais très efficace ! [[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]] ==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 simultanné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]] 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 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. Par contre, c'est autre chose sur les processeurs superscalaires larges. Vu qu'ils travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares. L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. La ''load/store queue'' reste une structure unique, mais on peut lui rajouter des ports. Et à ce peit jeu, il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire. [[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]] 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. Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre. Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Le Pentium 4 était dans ce cas, mais il était un peu seul dans sa catégorie. Un cas similaire est celui où l'unité mémoire est alimentée par une fenêtre de micro-opération mélangeant instructions mémoire et entières. Les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. Dans ce cas, l'émission multiple est rarement implémentée. Le cas suivant a une file pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique. Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui pouvaient seulement émettre une lecture en même temps qu'une écriture. Ils avaient deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse. [[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]] Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture demande d'utiliser une file d'écriture multiport, ce qui est assez compliqué. Implémenter des lectures en plus est plus simple : il suffit de rajouter une seconde file de µops LOAD et une unité de calcul, alimentées via un second port d'émission. Il est aussi possible de rajouter un second port de lecture au cache, mais ce n'est pas obligatoire. Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse. De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports de lecture et d'écriture car il y a plus de lecture que d'écritures. Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. : Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse. [[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]] Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettent plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail. ==Les optimisations des processeurs superscalaires larges== Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul. ===L'étape de chargement d'un CPU superscalaire large=== Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 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 autoaligné.]] Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements ! [[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]] Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez 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 microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total). Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois 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 microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement. Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations. ===La micro-fusion des processeurs Intel et AMD=== La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''. Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''. Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout. La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Le parallélisme mémoire | prevText=Le parallélisme mémoire | next=Exemples de microarchitectures CPU : le cas du x86 | nextText=Exemples de microarchitectures CPU : le cas du x86 }} </noinclude> 0a5u53jhmuvox69sk97a9nbk0ico8h5 772733 772732 2026-09-21T19:12:00Z Mewtow 31375 /* L'émission multiple des micro-opérations entières */ 772733 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. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps. 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. 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. Deux branchements consécutifs sont quant à eux plus fréquents, mais exécuter deux branchements en même temps pose un problème : que faire quand les deux sont pris ? Deux accès mémoire consécutifs est assez rare. La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs sont systématiquement supportées, car dupliquer des ALU 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 et 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'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. Par exemple, l'unité de chargement charge deux fois plus d'instructions, mais n'est pas modifiée en profondeur. L'unité d'émission et les circuits d'exécution dans le désordre sont eux aussi altérés. Les unités de calcul sont souvent dupliquées, sauf sur les tout premiers CPU superscalaires. {|class="wikitable" |- ! rowspan="2" | Processeur sans émission multiple | rowspan="2" | Chargement | rowspan="2" | Décodage | rowspan="2" | Renommage | rowspan="2" | Émission | Exécution / ALU | rowspan="2" | ''Commit''/ROB |- | Exécution / ALU |- ! rowspan="4" | Processeur superscalaire | rowspan="4" | Chargement | rowspan="2" | Décodage | rowspan="4" | Renommage | rowspan="4" | Émission | Exécution / ALU | rowspan="4" | ''Commit''/ROB |- | Exécution / ALU |- | rowspan="2" | Décodage | Exécution / ALU |- | Exécution / ALU |} Voyons ce qu'il en est dans le détail. ===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. 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. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant. 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. ===Le chargement de plusieurs instructions consécutives=== Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large. Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence. Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas. Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement non-pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc. [[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]] ===Le décodage parallèle des instructions=== Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits. Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur. Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! ===L'unité d'émission d'un processeur superscalaire=== La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. 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 4 instructions, compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément deux 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. 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. : 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 microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle. 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. ==L'évolution historique des processeurs superscalaires== Les tout premiers processeurs superscalaires avaient un budget en transistors très limité, ce qui fait que les contraintes d'appariement étaient très strictes. Et au-delà de ça, ils ne faisaient que mieux utiliser des unités de calcul déjà présentes, ils ne dupliquaient aucune unité de calcul. Prenons un processeur non-superscalaire simple, illustré ci-dessous. Il contient : une unité de chargement, un décodeur, une unité d'émission, une ALU, une FPU, les bancs de registre entier et flottants, et une unité mémoire. Nous avons placé l'unité mémoire à la suite de l'ALU entière, car ce sera important pour les explications qui vont suivre. [[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]] Les processeurs superscalaires historiques modifiaient assez peu le schéma précédent. L'unité de chargement était adaptée pour lire plusieurs instructions depuis la mémoire, il y avait un plusieurs décodeurs, l'unité d'émission était un peu modifiée. Mais aucune unité de calcul n'était dupliquée. A la rigueur, gérer la triple émission avec les branchements demandait parfois d'ajouter des ports de lecture sur le banc de registre généraux, pas plus. L'évolution des CPU superscalaires historiques s'est faite en quatre temps, avec des design de départ simples, qui ont progressivement évolués. Les étapes portent les noms de : double émission entière-flottante, d'exécution parallèle des branchements, d'émission multiple des opérations entières. Après la quatrième étape, les CPU superscalaires sont devenus très variés, au point de ne pas pouvoir être classés simplement. ===La double émission entière-flottante=== Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est d'émettre une micro-opération entière en même temps qu'une micro-opé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, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité. La plupart des premiers processeurs superscalaires étaient de ce type. Les exemples les plus notables sont les processeurs POWER 1 et ses dérivés comme le ''RISC Single Chip''. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle. [[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]] La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisations visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate]. ===L'émission parallèle des branchements=== Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur. 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]] Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications et les divisions ? Pour étudier ces questions, nous allons prendre le cas d'un processeur capable d’émettre trois opérations en même temps : deux opérations entières, et une flottante, potentiellement d'autres. {|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 |} 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|Emission multiple des opérations entières, implémentation courante]] Pour les branchements, deux solutions sont possibles. Avec la première, il y a émission parallèle des branchements, à savoir qu'il est possible d'émettre en branchement en parallèle de deux-trois instructions entières. Et cela impose la présence d'une unité de branchement séparée, avec son port d'émission. Avec la seconde, les branchements sont traités comme des instructions entières. Soit les branchements sont exécutés dans une ALU entière, soit il y a une unité de branchement reliée à un port d'émission pour les opérations entières. [[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Emission série et parallèle des branchements]] En général, les branchements sont exécutés comme des opérations entières. Cela permet d'économiser une unité de calcul et beaucoup de transistors dans l'unité d'émission/chargement/... {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 |- | * ALU entière * Multiplieur | * ALU entière * Barrel shifter * Unité de branchement | FPU |- | * Additions, soustractions, opérations bit à bit, etc. * Multiplication | * Additions, soustractions, opérations bit à bit, etc. * Décalages et rotations * Branchements |Opération flottante |} ===Les CPU superscalaires à plusieurs unités spécialisées=== L'évolution des CPU superscalaires s'est faite en quatre temps, et nous en sommes à la quatrième étape. Et cette quatrième étape ne correspond en réalité pas à grand chose de concret, ni à une évolution technique bien précise. Elle correspond en réalité à l'arrivée de processeur superscalaires très variés, qui n'ont pas grand chose en commun. Le premier CPU superscalaires utilisés dans les PC étaient le Pentium. Et c'était un CPU double émission un peu particulier. Il n'implémentait pas la double émission entière-flottante, mais il avait deux ALU entières. En clair, rien à voir avec une sorte d'évolution des designs précédents. Et il n'est pas le seul, la majorité des CPU superscalaire qui ont suivi ont privilégié l'ajout d'ALU entières. La raison est que les programmes informatiques utilisent beaucoup plus d'additions/soustractions que le reste. Il y a environ 5 opérations entières de base pour une multiplication entière, le ratio est le même quand on remplace la multiplication par un branchement ou un accès mémoire. En conséquence, les ALU entières ont été dupliquées en premier, et dans de nombreux exemplaires. Reste à voir comment a été dupliqué le reste. Les multiplieurs n'ont pas été dupliqués, sauf sur les processeurs superscalaires très larges. La raison est qu'il est rare de devoir exécuter deux multiplications à la fois. Et c'est la même chose pour le ''barrel shifter''. Deux branchements consécutifs sont quant à eux plus fréquents, c'est même assez courant. Aussi, les unités de branchement ont été dupliquées en second lieu, après les ALU entières. Avoir plusieurs accès mémoire proches est assez courant, surtout sur les CPU de type CISC. Aussi, les unités mémoire ont été dupliquées en troisième lieu. Pour résumer, dupliquer des ALU entières est donc la priorité, suivi par la duplication des unités de branchement, puis celle des accès mémoire, pour terminer par le multiplieur et le ''barrel shifter''. La FPU est un peu à part. La '''double émission flottante''' a été utilisée sur les processeurs Alpha. L'idée est d'émettre deux opérations flottantes à la fois. Sauf qu'une implémentation naïve demanderait de dupliquer la FPU, ce qui aurait un cout en circuit rédhibitoire. Pour rappel, les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant. L'idée est d'avoir des ports d'émission séparés pour l'additionneur flottant et le multiplieur flottant. Le processeur peut alors émettre une addition flottante en même temps qu'une multiplication flottante. Les autres circuits de calcul flottant sont répartis sur ces deux ports d'émission flottants. L'avantage est que cela se marie bien avec l'usage d'une fenêtre d'instruction séparée pour les opérations flottantes. La fenêtre d'instruction a alors deux ports séparés, au lieu d'un seul. Rajouter un second port d'émission flottant n'est pas trop un problème, car le cout lié à l'ajout d'un port n'est pas linéaire. Passer de un port à deux a un cout tolérable, bien plus que de passer de 3 ports à 4 ou de 4 à 5. Il est possible de dupliquer l'additionneur et le multiplieur flottant, ce qui permet d'émettre deux additions et multiplications flottantes en même temps. C'est ce qui est fait sur les processeurs AMD d'architecture Zen 1 et 2. Ils ont deux additionneurs flottants par cœur, deux multiplieurs flottants, chacun avec leur propre port d'émission. Les performances en calcul flottant sont assez impressionnantes pour un processeur de l'époque. [[File:ZEN - émission multiple flottante.png|centre|vignette|upright=2|Microarchitecture Zen 1 d'AMD.]] Pour le moment, nous n'avons vu que des cas où un port d'émission est relié soit à des ALU entières, soit à des FPU, pas aux deux. Mais il existe des processeurs où un même port d'émission alimente à la fois une ALU entière et une FPU. Par exemple, on peut relier un additionneur flottant sur le même port qu'une ALU entière. Il faut noter que cela implique une fenêtre d'instruction centralisée, capable de mettre en attente micro-opérations entières et flottantes. Un exemple est celui des processeurs Core 2 Duo. Ils disposent de 6 ports d'émission, dont 3 ports dédiés à l'unité mémoire. Les 3 ports restants alimentent chacun : une ALU entière, un circuit de calcul flottant et une unité de calcul SSE (une unité de calcul SIMD, qu'on abordera dans quelques chapitres). * Le premier port alimente une ALU entière simple et un multiplieur/diviseur flottant. * Le second alimente une ALU entière, un multiplieur entier et un additionneur flottant. * Le troisième alimente une ALU entière, sans circuit flottant dédié. Dans le même genre, il est possible de partager un port d'émission entre l'unité mémoire et une ALU entière. Cela permet d'utiliser l'ALU entière pour les calculs d'adresse, ce qui évite d'avoir à utiliser une unité de calcul d'adresse distincte. Un exemple est celui du processeur superscalaire double émission Power PC 440. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. L'organisation en question permet soit d'émettre un accès mémoire en même temps qu'une opération entière, soit d'émettre deux opérations entières simples, soit d’émettre une multiplication et une addition/soustraction/comparaison. Une organisation simple, mais très efficace ! [[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]] ==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 simultanné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]] 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 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. Par contre, c'est autre chose sur les processeurs superscalaires larges. Vu qu'ils travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares. L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. La ''load/store queue'' reste une structure unique, mais on peut lui rajouter des ports. Et à ce peit jeu, il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire. [[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]] 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. Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre. Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Le Pentium 4 était dans ce cas, mais il était un peu seul dans sa catégorie. Un cas similaire est celui où l'unité mémoire est alimentée par une fenêtre de micro-opération mélangeant instructions mémoire et entières. Les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. Dans ce cas, l'émission multiple est rarement implémentée. Le cas suivant a une file pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique. Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui pouvaient seulement émettre une lecture en même temps qu'une écriture. Ils avaient deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse. [[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]] Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture demande d'utiliser une file d'écriture multiport, ce qui est assez compliqué. Implémenter des lectures en plus est plus simple : il suffit de rajouter une seconde file de µops LOAD et une unité de calcul, alimentées via un second port d'émission. Il est aussi possible de rajouter un second port de lecture au cache, mais ce n'est pas obligatoire. Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse. De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports de lecture et d'écriture car il y a plus de lecture que d'écritures. Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. : Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse. [[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]] Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettent plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail. ==Les optimisations des processeurs superscalaires larges== Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul. ===L'étape de chargement d'un CPU superscalaire large=== Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 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 autoaligné.]] Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements ! [[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]] Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez 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 microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total). Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois 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 microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement. Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations. ===La micro-fusion des processeurs Intel et AMD=== La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''. Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''. Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout. La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Le parallélisme mémoire | prevText=Le parallélisme mémoire | next=Exemples de microarchitectures CPU : le cas du x86 | nextText=Exemples de microarchitectures CPU : le cas du x86 }} </noinclude> 2020jl3w804jshkok5qzvu4byajes2q 772734 772733 2026-09-21T19:14:05Z Mewtow 31375 /* L'émission multiple des micro-opérations entières */ 772734 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. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps. 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. 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. Deux branchements consécutifs sont quant à eux plus fréquents, mais exécuter deux branchements en même temps pose un problème : que faire quand les deux sont pris ? Deux accès mémoire consécutifs est assez rare. La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs sont systématiquement supportées, car dupliquer des ALU 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 et 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'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. Par exemple, l'unité de chargement charge deux fois plus d'instructions, mais n'est pas modifiée en profondeur. L'unité d'émission et les circuits d'exécution dans le désordre sont eux aussi altérés. Les unités de calcul sont souvent dupliquées, sauf sur les tout premiers CPU superscalaires. {|class="wikitable" |- ! rowspan="2" | Processeur sans émission multiple | rowspan="2" | Chargement | rowspan="2" | Décodage | rowspan="2" | Renommage | rowspan="2" | Émission | Exécution / ALU | rowspan="2" | ''Commit''/ROB |- | Exécution / ALU |- ! rowspan="4" | Processeur superscalaire | rowspan="4" | Chargement | rowspan="2" | Décodage | rowspan="4" | Renommage | rowspan="4" | Émission | Exécution / ALU | rowspan="4" | ''Commit''/ROB |- | Exécution / ALU |- | rowspan="2" | Décodage | Exécution / ALU |- | Exécution / ALU |} Voyons ce qu'il en est dans le détail. ===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. 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. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant. 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. ===Le chargement de plusieurs instructions consécutives=== Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large. Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence. Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas. Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement non-pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc. [[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]] ===Le décodage parallèle des instructions=== Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits. Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur. Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! ===L'unité d'émission d'un processeur superscalaire=== La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. 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 4 instructions, compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément deux 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. 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. : 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 microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle. 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. ==L'évolution historique des processeurs superscalaires== Les tout premiers processeurs superscalaires avaient un budget en transistors très limité, ce qui fait que les contraintes d'appariement étaient très strictes. Et au-delà de ça, ils ne faisaient que mieux utiliser des unités de calcul déjà présentes, ils ne dupliquaient aucune unité de calcul. Prenons un processeur non-superscalaire simple, illustré ci-dessous. Il contient : une unité de chargement, un décodeur, une unité d'émission, une ALU, une FPU, les bancs de registre entier et flottants, et une unité mémoire. Nous avons placé l'unité mémoire à la suite de l'ALU entière, car ce sera important pour les explications qui vont suivre. [[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]] Les processeurs superscalaires historiques modifiaient assez peu le schéma précédent. L'unité de chargement était adaptée pour lire plusieurs instructions depuis la mémoire, il y avait un plusieurs décodeurs, l'unité d'émission était un peu modifiée. Mais aucune unité de calcul n'était dupliquée. A la rigueur, gérer la triple émission avec les branchements demandait parfois d'ajouter des ports de lecture sur le banc de registre généraux, pas plus. L'évolution des CPU superscalaires historiques s'est faite en quatre temps, avec des design de départ simples, qui ont progressivement évolués. Les étapes portent les noms de : double émission entière-flottante, d'exécution parallèle des branchements, d'émission multiple des opérations entières. Après la quatrième étape, les CPU superscalaires sont devenus très variés, au point de ne pas pouvoir être classés simplement. ===La double émission entière-flottante=== Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est d'émettre une micro-opération entière en même temps qu'une micro-opé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, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité. La plupart des premiers processeurs superscalaires étaient de ce type. Les exemples les plus notables sont les processeurs POWER 1 et ses dérivés comme le ''RISC Single Chip''. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle. [[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]] La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisations visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate]. ===L'émission parallèle des branchements=== Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur. 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|Emission multiple des opérations entières, implémentation courante]] Pour les branchements, deux solutions sont possibles. Avec la première, il y a émission parallèle des branchements, à savoir qu'il est possible d'émettre en branchement en parallèle de deux-trois instructions entières. Et cela impose la présence d'une unité de branchement séparée, avec son port d'émission. Avec la seconde, les branchements sont traités comme des instructions entières. Soit les branchements sont exécutés dans une ALU entière, soit il y a une unité de branchement reliée à un port d'émission pour les opérations entières. [[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Emission série et parallèle des branchements]] En général, les branchements sont exécutés comme des opérations entières. Cela permet d'économiser une unité de calcul et beaucoup de transistors dans l'unité d'émission/chargement/... {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 |- | * ALU entière * Multiplieur | * ALU entière * Barrel shifter * Unité de branchement | FPU |- | * Additions, soustractions, opérations bit à bit, etc. * Multiplication | * Additions, soustractions, opérations bit à bit, etc. * Décalages et rotations * Branchements |Opération flottante |} ===Les CPU superscalaires à plusieurs unités spécialisées=== L'évolution des CPU superscalaires s'est faite en quatre temps, et nous en sommes à la quatrième étape. Et cette quatrième étape ne correspond en réalité pas à grand chose de concret, ni à une évolution technique bien précise. Elle correspond en réalité à l'arrivée de processeur superscalaires très variés, qui n'ont pas grand chose en commun. Le premier CPU superscalaires utilisés dans les PC étaient le Pentium. Et c'était un CPU double émission un peu particulier. Il n'implémentait pas la double émission entière-flottante, mais il avait deux ALU entières. En clair, rien à voir avec une sorte d'évolution des designs précédents. Et il n'est pas le seul, la majorité des CPU superscalaire qui ont suivi ont privilégié l'ajout d'ALU entières. La raison est que les programmes informatiques utilisent beaucoup plus d'additions/soustractions que le reste. Il y a environ 5 opérations entières de base pour une multiplication entière, le ratio est le même quand on remplace la multiplication par un branchement ou un accès mémoire. En conséquence, les ALU entières ont été dupliquées en premier, et dans de nombreux exemplaires. Reste à voir comment a été dupliqué le reste. Les multiplieurs n'ont pas été dupliqués, sauf sur les processeurs superscalaires très larges. La raison est qu'il est rare de devoir exécuter deux multiplications à la fois. Et c'est la même chose pour le ''barrel shifter''. Deux branchements consécutifs sont quant à eux plus fréquents, c'est même assez courant. Aussi, les unités de branchement ont été dupliquées en second lieu, après les ALU entières. Avoir plusieurs accès mémoire proches est assez courant, surtout sur les CPU de type CISC. Aussi, les unités mémoire ont été dupliquées en troisième lieu. Pour résumer, dupliquer des ALU entières est donc la priorité, suivi par la duplication des unités de branchement, puis celle des accès mémoire, pour terminer par le multiplieur et le ''barrel shifter''. La FPU est un peu à part. La '''double émission flottante''' a été utilisée sur les processeurs Alpha. L'idée est d'émettre deux opérations flottantes à la fois. Sauf qu'une implémentation naïve demanderait de dupliquer la FPU, ce qui aurait un cout en circuit rédhibitoire. Pour rappel, les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant. L'idée est d'avoir des ports d'émission séparés pour l'additionneur flottant et le multiplieur flottant. Le processeur peut alors émettre une addition flottante en même temps qu'une multiplication flottante. Les autres circuits de calcul flottant sont répartis sur ces deux ports d'émission flottants. L'avantage est que cela se marie bien avec l'usage d'une fenêtre d'instruction séparée pour les opérations flottantes. La fenêtre d'instruction a alors deux ports séparés, au lieu d'un seul. Rajouter un second port d'émission flottant n'est pas trop un problème, car le cout lié à l'ajout d'un port n'est pas linéaire. Passer de un port à deux a un cout tolérable, bien plus que de passer de 3 ports à 4 ou de 4 à 5. Il est possible de dupliquer l'additionneur et le multiplieur flottant, ce qui permet d'émettre deux additions et multiplications flottantes en même temps. C'est ce qui est fait sur les processeurs AMD d'architecture Zen 1 et 2. Ils ont deux additionneurs flottants par cœur, deux multiplieurs flottants, chacun avec leur propre port d'émission. Les performances en calcul flottant sont assez impressionnantes pour un processeur de l'époque. [[File:ZEN - émission multiple flottante.png|centre|vignette|upright=2|Microarchitecture Zen 1 d'AMD.]] Pour le moment, nous n'avons vu que des cas où un port d'émission est relié soit à des ALU entières, soit à des FPU, pas aux deux. Mais il existe des processeurs où un même port d'émission alimente à la fois une ALU entière et une FPU. Par exemple, on peut relier un additionneur flottant sur le même port qu'une ALU entière. Il faut noter que cela implique une fenêtre d'instruction centralisée, capable de mettre en attente micro-opérations entières et flottantes. Un exemple est celui des processeurs Core 2 Duo. Ils disposent de 6 ports d'émission, dont 3 ports dédiés à l'unité mémoire. Les 3 ports restants alimentent chacun : une ALU entière, un circuit de calcul flottant et une unité de calcul SSE (une unité de calcul SIMD, qu'on abordera dans quelques chapitres). * Le premier port alimente une ALU entière simple et un multiplieur/diviseur flottant. * Le second alimente une ALU entière, un multiplieur entier et un additionneur flottant. * Le troisième alimente une ALU entière, sans circuit flottant dédié. Dans le même genre, il est possible de partager un port d'émission entre l'unité mémoire et une ALU entière. Cela permet d'utiliser l'ALU entière pour les calculs d'adresse, ce qui évite d'avoir à utiliser une unité de calcul d'adresse distincte. Un exemple est celui du processeur superscalaire double émission Power PC 440. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. L'organisation en question permet soit d'émettre un accès mémoire en même temps qu'une opération entière, soit d'émettre deux opérations entières simples, soit d’émettre une multiplication et une addition/soustraction/comparaison. Une organisation simple, mais très efficace ! [[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]] ==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 simultanné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]] 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 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. Par contre, c'est autre chose sur les processeurs superscalaires larges. Vu qu'ils travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares. L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. La ''load/store queue'' reste une structure unique, mais on peut lui rajouter des ports. Et à ce peit jeu, il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire. [[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]] 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. Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre. Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Le Pentium 4 était dans ce cas, mais il était un peu seul dans sa catégorie. Un cas similaire est celui où l'unité mémoire est alimentée par une fenêtre de micro-opération mélangeant instructions mémoire et entières. Les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. Dans ce cas, l'émission multiple est rarement implémentée. Le cas suivant a une file pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique. Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui pouvaient seulement émettre une lecture en même temps qu'une écriture. Ils avaient deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse. [[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]] Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture demande d'utiliser une file d'écriture multiport, ce qui est assez compliqué. Implémenter des lectures en plus est plus simple : il suffit de rajouter une seconde file de µops LOAD et une unité de calcul, alimentées via un second port d'émission. Il est aussi possible de rajouter un second port de lecture au cache, mais ce n'est pas obligatoire. Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse. De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports de lecture et d'écriture car il y a plus de lecture que d'écritures. Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. : Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse. [[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]] Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettent plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail. ==Les optimisations des processeurs superscalaires larges== Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul. ===L'étape de chargement d'un CPU superscalaire large=== Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 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 autoaligné.]] Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements ! [[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]] Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez 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 microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total). Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois 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 microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement. Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations. ===La micro-fusion des processeurs Intel et AMD=== La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''. Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''. Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout. La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Le parallélisme mémoire | prevText=Le parallélisme mémoire | next=Exemples de microarchitectures CPU : le cas du x86 | nextText=Exemples de microarchitectures CPU : le cas du x86 }} </noinclude> 2mu68yrjm0yyhivdf1eax1ug1pdv1rj 772735 772734 2026-09-21T19:15:25Z Mewtow 31375 /* L'émission parallèle des accès mémoire */ 772735 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. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps. 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. 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. Deux branchements consécutifs sont quant à eux plus fréquents, mais exécuter deux branchements en même temps pose un problème : que faire quand les deux sont pris ? Deux accès mémoire consécutifs est assez rare. La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs sont systématiquement supportées, car dupliquer des ALU 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 et 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'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. Par exemple, l'unité de chargement charge deux fois plus d'instructions, mais n'est pas modifiée en profondeur. L'unité d'émission et les circuits d'exécution dans le désordre sont eux aussi altérés. Les unités de calcul sont souvent dupliquées, sauf sur les tout premiers CPU superscalaires. {|class="wikitable" |- ! rowspan="2" | Processeur sans émission multiple | rowspan="2" | Chargement | rowspan="2" | Décodage | rowspan="2" | Renommage | rowspan="2" | Émission | Exécution / ALU | rowspan="2" | ''Commit''/ROB |- | Exécution / ALU |- ! rowspan="4" | Processeur superscalaire | rowspan="4" | Chargement | rowspan="2" | Décodage | rowspan="4" | Renommage | rowspan="4" | Émission | Exécution / ALU | rowspan="4" | ''Commit''/ROB |- | Exécution / ALU |- | rowspan="2" | Décodage | Exécution / ALU |- | Exécution / ALU |} Voyons ce qu'il en est dans le détail. ===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. 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. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant. 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. ===Le chargement de plusieurs instructions consécutives=== Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large. Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence. Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas. Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement non-pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc. [[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]] ===Le décodage parallèle des instructions=== Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits. Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur. Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! ===L'unité d'émission d'un processeur superscalaire=== La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. 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 4 instructions, compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément deux 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. 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. : 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 microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle. 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. ==L'évolution historique des processeurs superscalaires== Les tout premiers processeurs superscalaires avaient un budget en transistors très limité, ce qui fait que les contraintes d'appariement étaient très strictes. Et au-delà de ça, ils ne faisaient que mieux utiliser des unités de calcul déjà présentes, ils ne dupliquaient aucune unité de calcul. Prenons un processeur non-superscalaire simple, illustré ci-dessous. Il contient : une unité de chargement, un décodeur, une unité d'émission, une ALU, une FPU, les bancs de registre entier et flottants, et une unité mémoire. Nous avons placé l'unité mémoire à la suite de l'ALU entière, car ce sera important pour les explications qui vont suivre. [[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]] Les processeurs superscalaires historiques modifiaient assez peu le schéma précédent. L'unité de chargement était adaptée pour lire plusieurs instructions depuis la mémoire, il y avait un plusieurs décodeurs, l'unité d'émission était un peu modifiée. Mais aucune unité de calcul n'était dupliquée. A la rigueur, gérer la triple émission avec les branchements demandait parfois d'ajouter des ports de lecture sur le banc de registre généraux, pas plus. L'évolution des CPU superscalaires historiques s'est faite en quatre temps, avec des design de départ simples, qui ont progressivement évolués. Les étapes portent les noms de : double émission entière-flottante, d'exécution parallèle des branchements, d'émission multiple des opérations entières. Après la quatrième étape, les CPU superscalaires sont devenus très variés, au point de ne pas pouvoir être classés simplement. ===La double émission entière-flottante=== Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est d'émettre une micro-opération entière en même temps qu'une micro-opé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, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité. La plupart des premiers processeurs superscalaires étaient de ce type. Les exemples les plus notables sont les processeurs POWER 1 et ses dérivés comme le ''RISC Single Chip''. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle. [[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]] La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisations visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate]. ===L'émission parallèle des branchements=== Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur. 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|Emission multiple des opérations entières, implémentation courante]] Pour les branchements, deux solutions sont possibles. Avec la première, il y a émission parallèle des branchements, à savoir qu'il est possible d'émettre en branchement en parallèle de deux-trois instructions entières. Et cela impose la présence d'une unité de branchement séparée, avec son port d'émission. Avec la seconde, les branchements sont traités comme des instructions entières. Soit les branchements sont exécutés dans une ALU entière, soit il y a une unité de branchement reliée à un port d'émission pour les opérations entières. [[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Emission série et parallèle des branchements]] En général, les branchements sont exécutés comme des opérations entières. Cela permet d'économiser une unité de calcul et beaucoup de transistors dans l'unité d'émission/chargement/... {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 |- | * ALU entière * Multiplieur | * ALU entière * Barrel shifter * Unité de branchement | FPU |- | * Additions, soustractions, opérations bit à bit, etc. * Multiplication | * Additions, soustractions, opérations bit à bit, etc. * Décalages et rotations * Branchements |Opération flottante |} ===Les CPU superscalaires à plusieurs unités spécialisées=== L'évolution des CPU superscalaires s'est faite en quatre temps, et nous en sommes à la quatrième étape. Et cette quatrième étape ne correspond en réalité pas à grand chose de concret, ni à une évolution technique bien précise. Elle correspond en réalité à l'arrivée de processeur superscalaires très variés, qui n'ont pas grand chose en commun. Le premier CPU superscalaires utilisés dans les PC étaient le Pentium. Et c'était un CPU double émission un peu particulier. Il n'implémentait pas la double émission entière-flottante, mais il avait deux ALU entières. En clair, rien à voir avec une sorte d'évolution des designs précédents. Et il n'est pas le seul, la majorité des CPU superscalaire qui ont suivi ont privilégié l'ajout d'ALU entières. La raison est que les programmes informatiques utilisent beaucoup plus d'additions/soustractions que le reste. Il y a environ 5 opérations entières de base pour une multiplication entière, le ratio est le même quand on remplace la multiplication par un branchement ou un accès mémoire. En conséquence, les ALU entières ont été dupliquées en premier, et dans de nombreux exemplaires. Reste à voir comment a été dupliqué le reste. Les multiplieurs n'ont pas été dupliqués, sauf sur les processeurs superscalaires très larges. La raison est qu'il est rare de devoir exécuter deux multiplications à la fois. Et c'est la même chose pour le ''barrel shifter''. Deux branchements consécutifs sont quant à eux plus fréquents, c'est même assez courant. Aussi, les unités de branchement ont été dupliquées en second lieu, après les ALU entières. Avoir plusieurs accès mémoire proches est assez courant, surtout sur les CPU de type CISC. Aussi, les unités mémoire ont été dupliquées en troisième lieu. Pour résumer, dupliquer des ALU entières est donc la priorité, suivi par la duplication des unités de branchement, puis celle des accès mémoire, pour terminer par le multiplieur et le ''barrel shifter''. La FPU est un peu à part. La '''double émission flottante''' a été utilisée sur les processeurs Alpha. L'idée est d'émettre deux opérations flottantes à la fois. Sauf qu'une implémentation naïve demanderait de dupliquer la FPU, ce qui aurait un cout en circuit rédhibitoire. Pour rappel, les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant. L'idée est d'avoir des ports d'émission séparés pour l'additionneur flottant et le multiplieur flottant. Le processeur peut alors émettre une addition flottante en même temps qu'une multiplication flottante. Les autres circuits de calcul flottant sont répartis sur ces deux ports d'émission flottants. L'avantage est que cela se marie bien avec l'usage d'une fenêtre d'instruction séparée pour les opérations flottantes. La fenêtre d'instruction a alors deux ports séparés, au lieu d'un seul. Rajouter un second port d'émission flottant n'est pas trop un problème, car le cout lié à l'ajout d'un port n'est pas linéaire. Passer de un port à deux a un cout tolérable, bien plus que de passer de 3 ports à 4 ou de 4 à 5. Il est possible de dupliquer l'additionneur et le multiplieur flottant, ce qui permet d'émettre deux additions et multiplications flottantes en même temps. C'est ce qui est fait sur les processeurs AMD d'architecture Zen 1 et 2. Ils ont deux additionneurs flottants par cœur, deux multiplieurs flottants, chacun avec leur propre port d'émission. Les performances en calcul flottant sont assez impressionnantes pour un processeur de l'époque. [[File:ZEN - émission multiple flottante.png|centre|vignette|upright=2|Microarchitecture Zen 1 d'AMD.]] Pour le moment, nous n'avons vu que des cas où un port d'émission est relié soit à des ALU entières, soit à des FPU, pas aux deux. Mais il existe des processeurs où un même port d'émission alimente à la fois une ALU entière et une FPU. Par exemple, on peut relier un additionneur flottant sur le même port qu'une ALU entière. Il faut noter que cela implique une fenêtre d'instruction centralisée, capable de mettre en attente micro-opérations entières et flottantes. Un exemple est celui des processeurs Core 2 Duo. Ils disposent de 6 ports d'émission, dont 3 ports dédiés à l'unité mémoire. Les 3 ports restants alimentent chacun : une ALU entière, un circuit de calcul flottant et une unité de calcul SSE (une unité de calcul SIMD, qu'on abordera dans quelques chapitres). * Le premier port alimente une ALU entière simple et un multiplieur/diviseur flottant. * Le second alimente une ALU entière, un multiplieur entier et un additionneur flottant. * Le troisième alimente une ALU entière, sans circuit flottant dédié. Dans le même genre, il est possible de partager un port d'émission entre l'unité mémoire et une ALU entière. Cela permet d'utiliser l'ALU entière pour les calculs d'adresse, ce qui évite d'avoir à utiliser une unité de calcul d'adresse distincte. Un exemple est celui du processeur superscalaire double émission Power PC 440. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. L'organisation en question permet soit d'émettre un accès mémoire en même temps qu'une opération entière, soit d'émettre deux opérations entières simples, soit d’émettre une multiplication et une addition/soustraction/comparaison. Une organisation simple, mais très efficace ! [[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]] ==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 simultanné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]] 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=== 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. Elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. La ''load/store queue'' reste une structure unique, mais on peut lui rajouter des ports. Et à ce peit jeu, il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire. [[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]] 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. Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre. Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Le Pentium 4 était dans ce cas, mais il était un peu seul dans sa catégorie. Un cas similaire est celui où l'unité mémoire est alimentée par une fenêtre de micro-opération mélangeant instructions mémoire et entières. Les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. Dans ce cas, l'émission multiple est rarement implémentée. Le cas suivant a une file pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique. Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui pouvaient seulement émettre une lecture en même temps qu'une écriture. Ils avaient deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse. [[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]] Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture demande d'utiliser une file d'écriture multiport, ce qui est assez compliqué. Implémenter des lectures en plus est plus simple : il suffit de rajouter une seconde file de µops LOAD et une unité de calcul, alimentées via un second port d'émission. Il est aussi possible de rajouter un second port de lecture au cache, mais ce n'est pas obligatoire. Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse. De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports de lecture et d'écriture car il y a plus de lecture que d'écritures. Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. : Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse. [[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]] Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettent plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail. ==Les optimisations des processeurs superscalaires larges== Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul. ===L'étape de chargement d'un CPU superscalaire large=== Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 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 autoaligné.]] Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements ! [[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]] Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez 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 microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total). Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois 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 microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement. Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations. ===La micro-fusion des processeurs Intel et AMD=== La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''. Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''. Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout. La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Le parallélisme mémoire | prevText=Le parallélisme mémoire | next=Exemples de microarchitectures CPU : le cas du x86 | nextText=Exemples de microarchitectures CPU : le cas du x86 }} </noinclude> dje3cm2yevon5uiugaf0o3gy1rku6da 772736 772735 2026-09-21T19:18:58Z Mewtow 31375 /* L'implémentation des processeurs superscalaires */ 772736 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. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps. 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. 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. Deux branchements consécutifs sont quant à eux plus fréquents, mais exécuter deux branchements en même temps pose un problème : que faire quand les deux sont pris ? Deux accès mémoire consécutifs est assez rare. La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs sont systématiquement supportées, car dupliquer des ALU 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 et 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'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. {|class="wikitable" |- ! rowspan="2" | Processeur sans émission multiple | rowspan="2" | Chargement | rowspan="2" | Décodage | rowspan="2" | Renommage | rowspan="2" | Émission | Exécution / ALU | rowspan="2" | ''Commit''/ROB |- | Exécution / ALU |- ! rowspan="4" | Processeur superscalaire | rowspan="4" | Chargement | rowspan="2" | Décodage | rowspan="4" | Renommage | rowspan="4" | Émission | Exécution / ALU | rowspan="4" | ''Commit''/ROB |- | Exécution / ALU |- | rowspan="2" | Décodage | Exécution / ALU |- | Exécution / ALU |} Voyons ce qu'il en est dans le détail. ===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. 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. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant. 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. ===Le chargement de plusieurs instructions consécutives=== Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large. Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence. Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas. Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement non-pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc. [[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]] ===Le décodage parallèle des instructions=== Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits. Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur. Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! ===L'unité d'émission d'un processeur superscalaire=== La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. 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 4 instructions, compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément deux 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. 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. : 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 microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle. 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. ==L'évolution historique des processeurs superscalaires== Les tout premiers processeurs superscalaires avaient un budget en transistors très limité, ce qui fait que les contraintes d'appariement étaient très strictes. Et au-delà de ça, ils ne faisaient que mieux utiliser des unités de calcul déjà présentes, ils ne dupliquaient aucune unité de calcul. Prenons un processeur non-superscalaire simple, illustré ci-dessous. Il contient : une unité de chargement, un décodeur, une unité d'émission, une ALU, une FPU, les bancs de registre entier et flottants, et une unité mémoire. Nous avons placé l'unité mémoire à la suite de l'ALU entière, car ce sera important pour les explications qui vont suivre. [[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]] Les processeurs superscalaires historiques modifiaient assez peu le schéma précédent. L'unité de chargement était adaptée pour lire plusieurs instructions depuis la mémoire, il y avait un plusieurs décodeurs, l'unité d'émission était un peu modifiée. Mais aucune unité de calcul n'était dupliquée. A la rigueur, gérer la triple émission avec les branchements demandait parfois d'ajouter des ports de lecture sur le banc de registre généraux, pas plus. L'évolution des CPU superscalaires historiques s'est faite en quatre temps, avec des design de départ simples, qui ont progressivement évolués. Les étapes portent les noms de : double émission entière-flottante, d'exécution parallèle des branchements, d'émission multiple des opérations entières. Après la quatrième étape, les CPU superscalaires sont devenus très variés, au point de ne pas pouvoir être classés simplement. ===La double émission entière-flottante=== Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est d'émettre une micro-opération entière en même temps qu'une micro-opé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, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité. La plupart des premiers processeurs superscalaires étaient de ce type. Les exemples les plus notables sont les processeurs POWER 1 et ses dérivés comme le ''RISC Single Chip''. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle. [[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]] La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisations visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate]. ===L'émission parallèle des branchements=== Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur. 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|Emission multiple des opérations entières, implémentation courante]] Pour les branchements, deux solutions sont possibles. Avec la première, il y a émission parallèle des branchements, à savoir qu'il est possible d'émettre en branchement en parallèle de deux-trois instructions entières. Et cela impose la présence d'une unité de branchement séparée, avec son port d'émission. Avec la seconde, les branchements sont traités comme des instructions entières. Soit les branchements sont exécutés dans une ALU entière, soit il y a une unité de branchement reliée à un port d'émission pour les opérations entières. [[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Emission série et parallèle des branchements]] En général, les branchements sont exécutés comme des opérations entières. Cela permet d'économiser une unité de calcul et beaucoup de transistors dans l'unité d'émission/chargement/... {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 |- | * ALU entière * Multiplieur | * ALU entière * Barrel shifter * Unité de branchement | FPU |- | * Additions, soustractions, opérations bit à bit, etc. * Multiplication | * Additions, soustractions, opérations bit à bit, etc. * Décalages et rotations * Branchements |Opération flottante |} ===Les CPU superscalaires à plusieurs unités spécialisées=== L'évolution des CPU superscalaires s'est faite en quatre temps, et nous en sommes à la quatrième étape. Et cette quatrième étape ne correspond en réalité pas à grand chose de concret, ni à une évolution technique bien précise. Elle correspond en réalité à l'arrivée de processeur superscalaires très variés, qui n'ont pas grand chose en commun. Le premier CPU superscalaires utilisés dans les PC étaient le Pentium. Et c'était un CPU double émission un peu particulier. Il n'implémentait pas la double émission entière-flottante, mais il avait deux ALU entières. En clair, rien à voir avec une sorte d'évolution des designs précédents. Et il n'est pas le seul, la majorité des CPU superscalaire qui ont suivi ont privilégié l'ajout d'ALU entières. La raison est que les programmes informatiques utilisent beaucoup plus d'additions/soustractions que le reste. Il y a environ 5 opérations entières de base pour une multiplication entière, le ratio est le même quand on remplace la multiplication par un branchement ou un accès mémoire. En conséquence, les ALU entières ont été dupliquées en premier, et dans de nombreux exemplaires. Reste à voir comment a été dupliqué le reste. Les multiplieurs n'ont pas été dupliqués, sauf sur les processeurs superscalaires très larges. La raison est qu'il est rare de devoir exécuter deux multiplications à la fois. Et c'est la même chose pour le ''barrel shifter''. Deux branchements consécutifs sont quant à eux plus fréquents, c'est même assez courant. Aussi, les unités de branchement ont été dupliquées en second lieu, après les ALU entières. Avoir plusieurs accès mémoire proches est assez courant, surtout sur les CPU de type CISC. Aussi, les unités mémoire ont été dupliquées en troisième lieu. Pour résumer, dupliquer des ALU entières est donc la priorité, suivi par la duplication des unités de branchement, puis celle des accès mémoire, pour terminer par le multiplieur et le ''barrel shifter''. La FPU est un peu à part. La '''double émission flottante''' a été utilisée sur les processeurs Alpha. L'idée est d'émettre deux opérations flottantes à la fois. Sauf qu'une implémentation naïve demanderait de dupliquer la FPU, ce qui aurait un cout en circuit rédhibitoire. Pour rappel, les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant. L'idée est d'avoir des ports d'émission séparés pour l'additionneur flottant et le multiplieur flottant. Le processeur peut alors émettre une addition flottante en même temps qu'une multiplication flottante. Les autres circuits de calcul flottant sont répartis sur ces deux ports d'émission flottants. L'avantage est que cela se marie bien avec l'usage d'une fenêtre d'instruction séparée pour les opérations flottantes. La fenêtre d'instruction a alors deux ports séparés, au lieu d'un seul. Rajouter un second port d'émission flottant n'est pas trop un problème, car le cout lié à l'ajout d'un port n'est pas linéaire. Passer de un port à deux a un cout tolérable, bien plus que de passer de 3 ports à 4 ou de 4 à 5. Il est possible de dupliquer l'additionneur et le multiplieur flottant, ce qui permet d'émettre deux additions et multiplications flottantes en même temps. C'est ce qui est fait sur les processeurs AMD d'architecture Zen 1 et 2. Ils ont deux additionneurs flottants par cœur, deux multiplieurs flottants, chacun avec leur propre port d'émission. Les performances en calcul flottant sont assez impressionnantes pour un processeur de l'époque. [[File:ZEN - émission multiple flottante.png|centre|vignette|upright=2|Microarchitecture Zen 1 d'AMD.]] Pour le moment, nous n'avons vu que des cas où un port d'émission est relié soit à des ALU entières, soit à des FPU, pas aux deux. Mais il existe des processeurs où un même port d'émission alimente à la fois une ALU entière et une FPU. Par exemple, on peut relier un additionneur flottant sur le même port qu'une ALU entière. Il faut noter que cela implique une fenêtre d'instruction centralisée, capable de mettre en attente micro-opérations entières et flottantes. Un exemple est celui des processeurs Core 2 Duo. Ils disposent de 6 ports d'émission, dont 3 ports dédiés à l'unité mémoire. Les 3 ports restants alimentent chacun : une ALU entière, un circuit de calcul flottant et une unité de calcul SSE (une unité de calcul SIMD, qu'on abordera dans quelques chapitres). * Le premier port alimente une ALU entière simple et un multiplieur/diviseur flottant. * Le second alimente une ALU entière, un multiplieur entier et un additionneur flottant. * Le troisième alimente une ALU entière, sans circuit flottant dédié. Dans le même genre, il est possible de partager un port d'émission entre l'unité mémoire et une ALU entière. Cela permet d'utiliser l'ALU entière pour les calculs d'adresse, ce qui évite d'avoir à utiliser une unité de calcul d'adresse distincte. Un exemple est celui du processeur superscalaire double émission Power PC 440. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. L'organisation en question permet soit d'émettre un accès mémoire en même temps qu'une opération entière, soit d'émettre deux opérations entières simples, soit d’émettre une multiplication et une addition/soustraction/comparaison. Une organisation simple, mais très efficace ! [[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]] ==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 simultanné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]] 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=== 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. Elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. La ''load/store queue'' reste une structure unique, mais on peut lui rajouter des ports. Et à ce peit jeu, il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire. [[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]] 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. Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre. Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Le Pentium 4 était dans ce cas, mais il était un peu seul dans sa catégorie. Un cas similaire est celui où l'unité mémoire est alimentée par une fenêtre de micro-opération mélangeant instructions mémoire et entières. Les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. Dans ce cas, l'émission multiple est rarement implémentée. Le cas suivant a une file pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique. Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui pouvaient seulement émettre une lecture en même temps qu'une écriture. Ils avaient deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse. [[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]] Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture demande d'utiliser une file d'écriture multiport, ce qui est assez compliqué. Implémenter des lectures en plus est plus simple : il suffit de rajouter une seconde file de µops LOAD et une unité de calcul, alimentées via un second port d'émission. Il est aussi possible de rajouter un second port de lecture au cache, mais ce n'est pas obligatoire. Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse. De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports de lecture et d'écriture car il y a plus de lecture que d'écritures. Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. : Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse. [[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]] Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettent plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail. ==Les optimisations des processeurs superscalaires larges== Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul. ===L'étape de chargement d'un CPU superscalaire large=== Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 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 autoaligné.]] Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements ! [[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]] Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez 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 microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total). Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois 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 microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement. Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations. ===La micro-fusion des processeurs Intel et AMD=== La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''. Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''. Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout. La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Le parallélisme mémoire | prevText=Le parallélisme mémoire | next=Exemples de microarchitectures CPU : le cas du x86 | nextText=Exemples de microarchitectures CPU : le cas du x86 }} </noinclude> a15nw08l9nnbcrzfkb9kd3zc5dghlzd 772737 772736 2026-09-21T19:24:19Z Mewtow 31375 /* L'implémentation des processeurs superscalaires */ 772737 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. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps. 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. 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. Deux branchements consécutifs sont quant à eux plus fréquents, mais exécuter deux branchements en même temps pose un problème : que faire quand les deux sont pris ? Deux accès mémoire consécutifs est assez rare. La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs sont systématiquement supportées, car dupliquer des ALU 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 et 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'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. 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. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant. 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. ===Le chargement de plusieurs instructions consécutives=== Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large. Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence. Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas. Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement non-pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc. [[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]] ===Le décodage parallèle des instructions=== Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits. Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur. Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! ===L'unité d'émission d'un processeur superscalaire=== La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. 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 4 instructions, compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément deux 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. 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. : 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 microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle. 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. ==L'évolution historique des processeurs superscalaires== Les tout premiers processeurs superscalaires avaient un budget en transistors très limité, ce qui fait que les contraintes d'appariement étaient très strictes. Et au-delà de ça, ils ne faisaient que mieux utiliser des unités de calcul déjà présentes, ils ne dupliquaient aucune unité de calcul. Prenons un processeur non-superscalaire simple, illustré ci-dessous. Il contient : une unité de chargement, un décodeur, une unité d'émission, une ALU, une FPU, les bancs de registre entier et flottants, et une unité mémoire. Nous avons placé l'unité mémoire à la suite de l'ALU entière, car ce sera important pour les explications qui vont suivre. [[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]] Les processeurs superscalaires historiques modifiaient assez peu le schéma précédent. L'unité de chargement était adaptée pour lire plusieurs instructions depuis la mémoire, il y avait un plusieurs décodeurs, l'unité d'émission était un peu modifiée. Mais aucune unité de calcul n'était dupliquée. A la rigueur, gérer la triple émission avec les branchements demandait parfois d'ajouter des ports de lecture sur le banc de registre généraux, pas plus. L'évolution des CPU superscalaires historiques s'est faite en quatre temps, avec des design de départ simples, qui ont progressivement évolués. Les étapes portent les noms de : double émission entière-flottante, d'exécution parallèle des branchements, d'émission multiple des opérations entières. Après la quatrième étape, les CPU superscalaires sont devenus très variés, au point de ne pas pouvoir être classés simplement. ===La double émission entière-flottante=== Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est d'émettre une micro-opération entière en même temps qu'une micro-opé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, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité. La plupart des premiers processeurs superscalaires étaient de ce type. Les exemples les plus notables sont les processeurs POWER 1 et ses dérivés comme le ''RISC Single Chip''. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle. [[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]] La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisations visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate]. ===L'émission parallèle des branchements=== Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur. 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|Emission multiple des opérations entières, implémentation courante]] Pour les branchements, deux solutions sont possibles. Avec la première, il y a émission parallèle des branchements, à savoir qu'il est possible d'émettre en branchement en parallèle de deux-trois instructions entières. Et cela impose la présence d'une unité de branchement séparée, avec son port d'émission. Avec la seconde, les branchements sont traités comme des instructions entières. Soit les branchements sont exécutés dans une ALU entière, soit il y a une unité de branchement reliée à un port d'émission pour les opérations entières. [[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Emission série et parallèle des branchements]] En général, les branchements sont exécutés comme des opérations entières. Cela permet d'économiser une unité de calcul et beaucoup de transistors dans l'unité d'émission/chargement/... {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 |- | * ALU entière * Multiplieur | * ALU entière * Barrel shifter * Unité de branchement | FPU |- | * Additions, soustractions, opérations bit à bit, etc. * Multiplication | * Additions, soustractions, opérations bit à bit, etc. * Décalages et rotations * Branchements |Opération flottante |} ===Les CPU superscalaires à plusieurs unités spécialisées=== L'évolution des CPU superscalaires s'est faite en quatre temps, et nous en sommes à la quatrième étape. Et cette quatrième étape ne correspond en réalité pas à grand chose de concret, ni à une évolution technique bien précise. Elle correspond en réalité à l'arrivée de processeur superscalaires très variés, qui n'ont pas grand chose en commun. Le premier CPU superscalaires utilisés dans les PC étaient le Pentium. Et c'était un CPU double émission un peu particulier. Il n'implémentait pas la double émission entière-flottante, mais il avait deux ALU entières. En clair, rien à voir avec une sorte d'évolution des designs précédents. Et il n'est pas le seul, la majorité des CPU superscalaire qui ont suivi ont privilégié l'ajout d'ALU entières. La raison est que les programmes informatiques utilisent beaucoup plus d'additions/soustractions que le reste. Il y a environ 5 opérations entières de base pour une multiplication entière, le ratio est le même quand on remplace la multiplication par un branchement ou un accès mémoire. En conséquence, les ALU entières ont été dupliquées en premier, et dans de nombreux exemplaires. Reste à voir comment a été dupliqué le reste. Les multiplieurs n'ont pas été dupliqués, sauf sur les processeurs superscalaires très larges. La raison est qu'il est rare de devoir exécuter deux multiplications à la fois. Et c'est la même chose pour le ''barrel shifter''. Deux branchements consécutifs sont quant à eux plus fréquents, c'est même assez courant. Aussi, les unités de branchement ont été dupliquées en second lieu, après les ALU entières. Avoir plusieurs accès mémoire proches est assez courant, surtout sur les CPU de type CISC. Aussi, les unités mémoire ont été dupliquées en troisième lieu. Pour résumer, dupliquer des ALU entières est donc la priorité, suivi par la duplication des unités de branchement, puis celle des accès mémoire, pour terminer par le multiplieur et le ''barrel shifter''. La FPU est un peu à part. La '''double émission flottante''' a été utilisée sur les processeurs Alpha. L'idée est d'émettre deux opérations flottantes à la fois. Sauf qu'une implémentation naïve demanderait de dupliquer la FPU, ce qui aurait un cout en circuit rédhibitoire. Pour rappel, les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant. L'idée est d'avoir des ports d'émission séparés pour l'additionneur flottant et le multiplieur flottant. Le processeur peut alors émettre une addition flottante en même temps qu'une multiplication flottante. Les autres circuits de calcul flottant sont répartis sur ces deux ports d'émission flottants. L'avantage est que cela se marie bien avec l'usage d'une fenêtre d'instruction séparée pour les opérations flottantes. La fenêtre d'instruction a alors deux ports séparés, au lieu d'un seul. Rajouter un second port d'émission flottant n'est pas trop un problème, car le cout lié à l'ajout d'un port n'est pas linéaire. Passer de un port à deux a un cout tolérable, bien plus que de passer de 3 ports à 4 ou de 4 à 5. Il est possible de dupliquer l'additionneur et le multiplieur flottant, ce qui permet d'émettre deux additions et multiplications flottantes en même temps. C'est ce qui est fait sur les processeurs AMD d'architecture Zen 1 et 2. Ils ont deux additionneurs flottants par cœur, deux multiplieurs flottants, chacun avec leur propre port d'émission. Les performances en calcul flottant sont assez impressionnantes pour un processeur de l'époque. [[File:ZEN - émission multiple flottante.png|centre|vignette|upright=2|Microarchitecture Zen 1 d'AMD.]] Pour le moment, nous n'avons vu que des cas où un port d'émission est relié soit à des ALU entières, soit à des FPU, pas aux deux. Mais il existe des processeurs où un même port d'émission alimente à la fois une ALU entière et une FPU. Par exemple, on peut relier un additionneur flottant sur le même port qu'une ALU entière. Il faut noter que cela implique une fenêtre d'instruction centralisée, capable de mettre en attente micro-opérations entières et flottantes. Un exemple est celui des processeurs Core 2 Duo. Ils disposent de 6 ports d'émission, dont 3 ports dédiés à l'unité mémoire. Les 3 ports restants alimentent chacun : une ALU entière, un circuit de calcul flottant et une unité de calcul SSE (une unité de calcul SIMD, qu'on abordera dans quelques chapitres). * Le premier port alimente une ALU entière simple et un multiplieur/diviseur flottant. * Le second alimente une ALU entière, un multiplieur entier et un additionneur flottant. * Le troisième alimente une ALU entière, sans circuit flottant dédié. Dans le même genre, il est possible de partager un port d'émission entre l'unité mémoire et une ALU entière. Cela permet d'utiliser l'ALU entière pour les calculs d'adresse, ce qui évite d'avoir à utiliser une unité de calcul d'adresse distincte. Un exemple est celui du processeur superscalaire double émission Power PC 440. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. L'organisation en question permet soit d'émettre un accès mémoire en même temps qu'une opération entière, soit d'émettre deux opérations entières simples, soit d’émettre une multiplication et une addition/soustraction/comparaison. Une organisation simple, mais très efficace ! [[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]] ==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 simultanné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]] 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=== 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. Elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. La ''load/store queue'' reste une structure unique, mais on peut lui rajouter des ports. Et à ce peit jeu, il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire. [[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]] 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. Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre. Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Le Pentium 4 était dans ce cas, mais il était un peu seul dans sa catégorie. Un cas similaire est celui où l'unité mémoire est alimentée par une fenêtre de micro-opération mélangeant instructions mémoire et entières. Les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. Dans ce cas, l'émission multiple est rarement implémentée. Le cas suivant a une file pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique. Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui pouvaient seulement émettre une lecture en même temps qu'une écriture. Ils avaient deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse. [[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]] Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture demande d'utiliser une file d'écriture multiport, ce qui est assez compliqué. Implémenter des lectures en plus est plus simple : il suffit de rajouter une seconde file de µops LOAD et une unité de calcul, alimentées via un second port d'émission. Il est aussi possible de rajouter un second port de lecture au cache, mais ce n'est pas obligatoire. Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse. De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports de lecture et d'écriture car il y a plus de lecture que d'écritures. Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. : Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse. [[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]] Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettent plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail. ==Les optimisations des processeurs superscalaires larges== Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul. ===L'étape de chargement d'un CPU superscalaire large=== Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 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 autoaligné.]] Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements ! [[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]] Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez 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 microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total). Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois 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 microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement. Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations. ===La micro-fusion des processeurs Intel et AMD=== La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''. Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''. Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout. La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Le parallélisme mémoire | prevText=Le parallélisme mémoire | next=Exemples de microarchitectures CPU : le cas du x86 | nextText=Exemples de microarchitectures CPU : le cas du x86 }} </noinclude> mls4mr6vl3rv0q8cxff6759h11ike8k 772738 772737 2026-09-21T19:29:04Z Mewtow 31375 /* L'évolution historique des processeurs superscalaires */ 772738 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. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps. 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. 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. Deux branchements consécutifs sont quant à eux plus fréquents, mais exécuter deux branchements en même temps pose un problème : que faire quand les deux sont pris ? Deux accès mémoire consécutifs est assez rare. La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs sont systématiquement supportées, car dupliquer des ALU 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 et 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'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. 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. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant. 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. ===Le chargement de plusieurs instructions consécutives=== Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large. Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence. Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas. Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement non-pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc. [[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]] ===Le décodage parallèle des instructions=== Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits. Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur. Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! ===L'unité d'émission d'un processeur superscalaire=== La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. 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 4 instructions, compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément deux 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. 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. : 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 microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle. 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. ==L'évolution historique des processeurs superscalaires== L'évolution des CPU superscalaires historiques 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. Leur historique distingue six étapes, mais je vais en omettre une et fusionner les deux dernières. Le résultat est une évolution en quatre temps, qui portent les noms de : * double émission entière-flottante ; * exécution parallèle des branchements ; * émission multiple des opérations entières. Les tout premiers processeurs superscalaires avaient un budget en transistors très limité, ce qui fait que les contraintes d'appariement étaient très strictes. Les deux premières étapes ne faisaient que mieux utiliser des unités de calcul déjà présentes, ils ne dupliquaient aucune unité de calcul. La troisième a ajouté des ALUs entières, pour améliorer un peu les performances. Après la quatrième étape, les CPU superscalaires sont devenus très variés, au point de ne pas pouvoir être classés simplement. ===La double émission entière-flottante=== Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est d'émettre une micro-opération entière en même temps qu'une micro-opé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, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité. La plupart des premiers processeurs superscalaires étaient de ce type. Les exemples les plus notables sont les processeurs POWER 1 et ses dérivés comme le ''RISC Single Chip''. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle. [[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]] La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisations visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate]. ===L'émission parallèle des branchements=== Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur. 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|Emission multiple des opérations entières, implémentation courante]] Pour les branchements, deux solutions sont possibles. Avec la première, il y a émission parallèle des branchements, à savoir qu'il est possible d'émettre en branchement en parallèle de deux-trois instructions entières. Et cela impose la présence d'une unité de branchement séparée, avec son port d'émission. Avec la seconde, les branchements sont traités comme des instructions entières. Soit les branchements sont exécutés dans une ALU entière, soit il y a une unité de branchement reliée à un port d'émission pour les opérations entières. [[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Emission série et parallèle des branchements]] En général, les branchements sont exécutés comme des opérations entières. Cela permet d'économiser une unité de calcul et beaucoup de transistors dans l'unité d'émission/chargement/... {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 |- | * ALU entière * Multiplieur | * ALU entière * Barrel shifter * Unité de branchement | FPU |- | * Additions, soustractions, opérations bit à bit, etc. * Multiplication | * Additions, soustractions, opérations bit à bit, etc. * Décalages et rotations * Branchements |Opération flottante |} ===Les CPU superscalaires à plusieurs unités spécialisées=== L'évolution des CPU superscalaires s'est faite en quatre temps, et nous en sommes à la quatrième étape. Et cette quatrième étape ne correspond en réalité pas à grand chose de concret, ni à une évolution technique bien précise. Elle correspond en réalité à l'arrivée de processeur superscalaires très variés, qui n'ont pas grand chose en commun. Le premier CPU superscalaires utilisés dans les PC étaient le Pentium. Et c'était un CPU double émission un peu particulier. Il n'implémentait pas la double émission entière-flottante, mais il avait deux ALU entières. En clair, rien à voir avec une sorte d'évolution des designs précédents. Et il n'est pas le seul, la majorité des CPU superscalaire qui ont suivi ont privilégié l'ajout d'ALU entières. La raison est que les programmes informatiques utilisent beaucoup plus d'additions/soustractions que le reste. Il y a environ 5 opérations entières de base pour une multiplication entière, le ratio est le même quand on remplace la multiplication par un branchement ou un accès mémoire. En conséquence, les ALU entières ont été dupliquées en premier, et dans de nombreux exemplaires. Reste à voir comment a été dupliqué le reste. Les multiplieurs n'ont pas été dupliqués, sauf sur les processeurs superscalaires très larges. La raison est qu'il est rare de devoir exécuter deux multiplications à la fois. Et c'est la même chose pour le ''barrel shifter''. Deux branchements consécutifs sont quant à eux plus fréquents, c'est même assez courant. Aussi, les unités de branchement ont été dupliquées en second lieu, après les ALU entières. Avoir plusieurs accès mémoire proches est assez courant, surtout sur les CPU de type CISC. Aussi, les unités mémoire ont été dupliquées en troisième lieu. Pour résumer, dupliquer des ALU entières est donc la priorité, suivi par la duplication des unités de branchement, puis celle des accès mémoire, pour terminer par le multiplieur et le ''barrel shifter''. La FPU est un peu à part. La '''double émission flottante''' a été utilisée sur les processeurs Alpha. L'idée est d'émettre deux opérations flottantes à la fois. Sauf qu'une implémentation naïve demanderait de dupliquer la FPU, ce qui aurait un cout en circuit rédhibitoire. Pour rappel, les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant. L'idée est d'avoir des ports d'émission séparés pour l'additionneur flottant et le multiplieur flottant. Le processeur peut alors émettre une addition flottante en même temps qu'une multiplication flottante. Les autres circuits de calcul flottant sont répartis sur ces deux ports d'émission flottants. L'avantage est que cela se marie bien avec l'usage d'une fenêtre d'instruction séparée pour les opérations flottantes. La fenêtre d'instruction a alors deux ports séparés, au lieu d'un seul. Rajouter un second port d'émission flottant n'est pas trop un problème, car le cout lié à l'ajout d'un port n'est pas linéaire. Passer de un port à deux a un cout tolérable, bien plus que de passer de 3 ports à 4 ou de 4 à 5. Il est possible de dupliquer l'additionneur et le multiplieur flottant, ce qui permet d'émettre deux additions et multiplications flottantes en même temps. C'est ce qui est fait sur les processeurs AMD d'architecture Zen 1 et 2. Ils ont deux additionneurs flottants par cœur, deux multiplieurs flottants, chacun avec leur propre port d'émission. Les performances en calcul flottant sont assez impressionnantes pour un processeur de l'époque. [[File:ZEN - émission multiple flottante.png|centre|vignette|upright=2|Microarchitecture Zen 1 d'AMD.]] Pour le moment, nous n'avons vu que des cas où un port d'émission est relié soit à des ALU entières, soit à des FPU, pas aux deux. Mais il existe des processeurs où un même port d'émission alimente à la fois une ALU entière et une FPU. Par exemple, on peut relier un additionneur flottant sur le même port qu'une ALU entière. Il faut noter que cela implique une fenêtre d'instruction centralisée, capable de mettre en attente micro-opérations entières et flottantes. Un exemple est celui des processeurs Core 2 Duo. Ils disposent de 6 ports d'émission, dont 3 ports dédiés à l'unité mémoire. Les 3 ports restants alimentent chacun : une ALU entière, un circuit de calcul flottant et une unité de calcul SSE (une unité de calcul SIMD, qu'on abordera dans quelques chapitres). * Le premier port alimente une ALU entière simple et un multiplieur/diviseur flottant. * Le second alimente une ALU entière, un multiplieur entier et un additionneur flottant. * Le troisième alimente une ALU entière, sans circuit flottant dédié. Dans le même genre, il est possible de partager un port d'émission entre l'unité mémoire et une ALU entière. Cela permet d'utiliser l'ALU entière pour les calculs d'adresse, ce qui évite d'avoir à utiliser une unité de calcul d'adresse distincte. Un exemple est celui du processeur superscalaire double émission Power PC 440. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. L'organisation en question permet soit d'émettre un accès mémoire en même temps qu'une opération entière, soit d'émettre deux opérations entières simples, soit d’émettre une multiplication et une addition/soustraction/comparaison. Une organisation simple, mais très efficace ! [[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]] ==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 simultanné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]] 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=== 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. Elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. La ''load/store queue'' reste une structure unique, mais on peut lui rajouter des ports. Et à ce peit jeu, il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire. [[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]] 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. Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre. Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Le Pentium 4 était dans ce cas, mais il était un peu seul dans sa catégorie. Un cas similaire est celui où l'unité mémoire est alimentée par une fenêtre de micro-opération mélangeant instructions mémoire et entières. Les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. Dans ce cas, l'émission multiple est rarement implémentée. Le cas suivant a une file pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique. Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui pouvaient seulement émettre une lecture en même temps qu'une écriture. Ils avaient deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse. [[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]] Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture demande d'utiliser une file d'écriture multiport, ce qui est assez compliqué. Implémenter des lectures en plus est plus simple : il suffit de rajouter une seconde file de µops LOAD et une unité de calcul, alimentées via un second port d'émission. Il est aussi possible de rajouter un second port de lecture au cache, mais ce n'est pas obligatoire. Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse. De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports de lecture et d'écriture car il y a plus de lecture que d'écritures. Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. : Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse. [[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]] Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettent plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail. ==Les optimisations des processeurs superscalaires larges== Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul. ===L'étape de chargement d'un CPU superscalaire large=== Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 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 autoaligné.]] Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements ! [[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]] Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez 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 microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total). Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois 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 microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement. Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations. ===La micro-fusion des processeurs Intel et AMD=== La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''. Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''. Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout. La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Le parallélisme mémoire | prevText=Le parallélisme mémoire | next=Exemples de microarchitectures CPU : le cas du x86 | nextText=Exemples de microarchitectures CPU : le cas du x86 }} </noinclude> q2xorsqpccgt14t69q2gxjqaex3gr45 772739 772738 2026-09-21T19:49:51Z Mewtow 31375 /* Les CPU superscalaires à plusieurs unités spécialisées */ 772739 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. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps. 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. 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. Deux branchements consécutifs sont quant à eux plus fréquents, mais exécuter deux branchements en même temps pose un problème : que faire quand les deux sont pris ? Deux accès mémoire consécutifs est assez rare. La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs sont systématiquement supportées, car dupliquer des ALU 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 et 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'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. 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. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant. 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. ===Le chargement de plusieurs instructions consécutives=== Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large. Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence. Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas. Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement non-pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc. [[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]] ===Le décodage parallèle des instructions=== Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits. Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur. Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! ===L'unité d'émission d'un processeur superscalaire=== La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. 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 4 instructions, compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément deux 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. 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. : 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 microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle. 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. ==L'évolution historique des processeurs superscalaires== L'évolution des CPU superscalaires historiques 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. Leur historique distingue six étapes, mais je vais en omettre une et fusionner les deux dernières. Le résultat est une évolution en quatre temps, qui portent les noms de : * double émission entière-flottante ; * exécution parallèle des branchements ; * émission multiple des opérations entières. Les tout premiers processeurs superscalaires avaient un budget en transistors très limité, ce qui fait que les contraintes d'appariement étaient très strictes. Les deux premières étapes ne faisaient que mieux utiliser des unités de calcul déjà présentes, ils ne dupliquaient aucune unité de calcul. La troisième a ajouté des ALUs entières, pour améliorer un peu les performances. Après la quatrième étape, les CPU superscalaires sont devenus très variés, au point de ne pas pouvoir être classés simplement. ===La double émission entière-flottante=== Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est d'émettre une micro-opération entière en même temps qu'une micro-opé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, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité. La plupart des premiers processeurs superscalaires étaient de ce type. Les exemples les plus notables sont les processeurs POWER 1 et ses dérivés comme le ''RISC Single Chip''. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle. [[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]] La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisations visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate]. ===L'émission parallèle des branchements=== Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur. 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|Emission multiple des opérations entières, implémentation courante]] Pour les branchements, deux solutions sont possibles. Avec la première, il y a émission parallèle des branchements, à savoir qu'il est possible d'émettre en branchement en parallèle de deux-trois instructions entières. Et cela impose la présence d'une unité de branchement séparée, avec son port d'émission. Avec la seconde, les branchements sont traités comme des instructions entières. Soit les branchements sont exécutés dans une ALU entière, soit il y a une unité de branchement reliée à un port d'émission pour les opérations entières. [[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Emission série et parallèle des branchements]] En général, les branchements sont exécutés comme des opérations entières. Cela permet d'économiser une unité de calcul et beaucoup de transistors dans l'unité d'émission/chargement/... {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 |- | * ALU entière * Multiplieur | * ALU entière * Barrel shifter * Unité de branchement | FPU |- | * Additions, soustractions, opérations bit à bit, etc. * Multiplication | * Additions, soustractions, opérations bit à bit, etc. * Décalages et rotations * Branchements |Opération flottante |} ===Les CPU superscalaires à plusieurs unités spécialisées=== L'évolution des CPU superscalaires s'est faite en quatre temps, et nous en sommes à la quatrième étape. Elle ne correspond en réalité pas à grand chose de concret, ni à une évolution technique bien précise. Elle correspond en réalité à l'arrivée de processeurs superscalaires très variés, qui n'ont pas grand chose en commun. Le premier CPU superscalaire grand public était le Pentium. Et c'était un CPU double émission qui n'implémentait pas la double émission entière-flottante, mais qui avait deux ALU entières. 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. Ils ont eux aussi privilégié l'ajout d'ALU entières. Les multiplieurs n'ont pas été dupliqués, sauf sur les processeurs superscalaires très larges. La raison est qu'il est rare de devoir exécuter deux multiplications à la fois. Et c'est la même chose pour le ''barrel shifter''. Pour donner quelques exemples, étudions les processeurs superscalaire Power PC. Il y a eu plusieurs modèles, avec des jeux d'instructions légèrement différents, mais aussi et surtout des microarchitectures assez différentes. Les premiers, avant le Power PC G4, étaient des CPU étroits, qui hésitaient entre 2, 3 et 4 voies. Par contre, à partir du G5, on passe à des CPU larges, avec 8 voies ! Le premier CPU PowerPC superscalaire était le PowerPC 601. Il avait une forme limitée de triple émission, identique à celle observée sur les CPU superscalaire historiques. Il pouvait émettre en même temps : une opération entière/mémoire, une opération flottante, et un branchement. Le PowerPC 603 utilisait une forme hybride entre double émission et émission parallèle des branchements. En clair, il pouvait émettre deux instructions + un branchement. Il intégrait 5 unités de calcul : une ALU entière, une FPU, l'unité de branchement, l'unité mémoire et une unité système pour gérer les registres de contrôle et les instructions particulières. Si on omet les branchements, il pouvait émettre une opération entière avec une autre instruction (non-entière, vu qu'il n'y a qu'une seule ALU). Il pouvait aussi émettre une instruction flottante et une µop mémoire simultanément. Il avait donc une réelle double émission, ce qui fait qu'il était qualifié de ''CPU 2 + Branch''. Le PowerPC 604 est lui passé ) la quadruple émission, avec émission parallèle des branchements. En clair, c'était un CPU de type ''4 + Branch''. Il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire, l'unité de branchement. La FPU et le multiplieur étaient sur le même port d'émission, mais c'était le seul à être partagé. En clair, le CPU pouvait émettre deux instructions entières, un accès mémoire, un branchement, et : soit une multiplication/division, soit une opération flottante. Le PowerPC 620 était similaire, sauf qu'il permettait d'émettre deux accès mémoire, ce qu'on n'a pas encore vu à ce stade du cours. Un exemple est celui du processeur superscalaire double émission Power PC 440. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. Une organisation simple, mais très efficace ! [[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]] La '''double émission flottante''' a été utilisée sur les processeurs Alpha. L'idée est d'émettre deux opérations flottantes à la fois. Sauf qu'une implémentation naïve demanderait de dupliquer la FPU, ce qui aurait un cout en circuit rédhibitoire. Pour rappel, les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant. L'idée est d'avoir des ports d'émission séparés pour l'additionneur flottant et le multiplieur flottant. Le processeur peut alors émettre une addition flottante en même temps qu'une multiplication flottante. Les autres circuits de calcul flottant sont répartis sur ces deux ports d'émission flottants. L'avantage est que cela se marie bien avec l'usage d'une fenêtre d'instruction séparée pour les opérations flottantes. La fenêtre d'instruction a alors deux ports séparés, au lieu d'un seul. Rajouter un second port d'émission flottant n'est pas trop un problème, car le cout lié à l'ajout d'un port n'est pas linéaire. Passer de un port à deux a un cout tolérable, bien plus que de passer de 3 ports à 4 ou de 4 à 5. Il est possible de dupliquer l'additionneur et le multiplieur flottant, ce qui permet d'émettre deux additions et multiplications flottantes en même temps. C'est ce qui est fait sur les processeurs AMD d'architecture Zen 1 et 2. Ils ont deux additionneurs flottants par cœur, deux multiplieurs flottants, chacun avec leur propre port d'émission. Les performances en calcul flottant sont assez impressionnantes pour un processeur de l'époque. [[File:ZEN - émission multiple flottante.png|centre|vignette|upright=2|Microarchitecture Zen 1 d'AMD.]] Pour le moment, nous n'avons vu que des cas où un port d'émission est relié soit à des ALU entières, soit à des FPU, pas aux deux. Mais il existe des processeurs où un même port d'émission alimente à la fois une ALU entière et une FPU. Par exemple, on peut relier un additionneur flottant sur le même port qu'une ALU entière. Il faut noter que cela implique une fenêtre d'instruction centralisée, capable de mettre en attente micro-opérations entières et flottantes. Un exemple est celui des processeurs Core 2 Duo. Ils disposent de 6 ports d'émission, dont 3 ports dédiés à l'unité mémoire. Les 3 ports restants alimentent chacun : une ALU entière, un circuit de calcul flottant et une unité de calcul SSE (une unité de calcul SIMD, qu'on abordera dans quelques chapitres). * Le premier port alimente une ALU entière simple et un multiplieur/diviseur flottant. * Le second alimente une ALU entière, un multiplieur entier et un additionneur flottant. * Le troisième alimente une ALU entière, sans circuit flottant dédié. ==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 simultanné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]] 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=== 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. Elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. La ''load/store queue'' reste une structure unique, mais on peut lui rajouter des ports. Et à ce peit jeu, il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire. [[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]] 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. Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre. Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Le Pentium 4 était dans ce cas, mais il était un peu seul dans sa catégorie. Un cas similaire est celui où l'unité mémoire est alimentée par une fenêtre de micro-opération mélangeant instructions mémoire et entières. Les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. Dans ce cas, l'émission multiple est rarement implémentée. Le cas suivant a une file pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique. Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui pouvaient seulement émettre une lecture en même temps qu'une écriture. Ils avaient deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse. [[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]] Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture demande d'utiliser une file d'écriture multiport, ce qui est assez compliqué. Implémenter des lectures en plus est plus simple : il suffit de rajouter une seconde file de µops LOAD et une unité de calcul, alimentées via un second port d'émission. Il est aussi possible de rajouter un second port de lecture au cache, mais ce n'est pas obligatoire. Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse. De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports de lecture et d'écriture car il y a plus de lecture que d'écritures. Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. : Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse. [[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]] Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettent plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail. ==Les optimisations des processeurs superscalaires larges== Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul. ===L'étape de chargement d'un CPU superscalaire large=== Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 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 autoaligné.]] Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements ! [[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]] Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez 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 microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total). Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois 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 microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement. Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations. ===La micro-fusion des processeurs Intel et AMD=== La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''. Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''. Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout. La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Le parallélisme mémoire | prevText=Le parallélisme mémoire | next=Exemples de microarchitectures CPU : le cas du x86 | nextText=Exemples de microarchitectures CPU : le cas du x86 }} </noinclude> rr09zlcu91z3d2656xmf5pdkyg61a87 772740 772739 2026-09-21T19:49:59Z Mewtow 31375 /* Les CPU superscalaires à plusieurs unités spécialisées */ 772740 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. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps. 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. 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. Deux branchements consécutifs sont quant à eux plus fréquents, mais exécuter deux branchements en même temps pose un problème : que faire quand les deux sont pris ? Deux accès mémoire consécutifs est assez rare. La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs sont systématiquement supportées, car dupliquer des ALU 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 et 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'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. 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. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant. 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. ===Le chargement de plusieurs instructions consécutives=== Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large. Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence. Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas. Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement non-pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc. [[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]] ===Le décodage parallèle des instructions=== Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits. Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur. Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! ===L'unité d'émission d'un processeur superscalaire=== La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. 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 4 instructions, compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément deux 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. 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. : 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 microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle. 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. ==L'évolution historique des processeurs superscalaires== L'évolution des CPU superscalaires historiques 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. Leur historique distingue six étapes, mais je vais en omettre une et fusionner les deux dernières. Le résultat est une évolution en quatre temps, qui portent les noms de : * double émission entière-flottante ; * exécution parallèle des branchements ; * émission multiple des opérations entières. Les tout premiers processeurs superscalaires avaient un budget en transistors très limité, ce qui fait que les contraintes d'appariement étaient très strictes. Les deux premières étapes ne faisaient que mieux utiliser des unités de calcul déjà présentes, ils ne dupliquaient aucune unité de calcul. La troisième a ajouté des ALUs entières, pour améliorer un peu les performances. Après la quatrième étape, les CPU superscalaires sont devenus très variés, au point de ne pas pouvoir être classés simplement. ===La double émission entière-flottante=== Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est d'émettre une micro-opération entière en même temps qu'une micro-opé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, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité. La plupart des premiers processeurs superscalaires étaient de ce type. Les exemples les plus notables sont les processeurs POWER 1 et ses dérivés comme le ''RISC Single Chip''. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle. [[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]] La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisations visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate]. ===L'émission parallèle des branchements=== Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur. 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|Emission multiple des opérations entières, implémentation courante]] Pour les branchements, deux solutions sont possibles. Avec la première, il y a émission parallèle des branchements, à savoir qu'il est possible d'émettre en branchement en parallèle de deux-trois instructions entières. Et cela impose la présence d'une unité de branchement séparée, avec son port d'émission. Avec la seconde, les branchements sont traités comme des instructions entières. Soit les branchements sont exécutés dans une ALU entière, soit il y a une unité de branchement reliée à un port d'émission pour les opérations entières. [[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Emission série et parallèle des branchements]] En général, les branchements sont exécutés comme des opérations entières. Cela permet d'économiser une unité de calcul et beaucoup de transistors dans l'unité d'émission/chargement/... {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 |- | * ALU entière * Multiplieur | * ALU entière * Barrel shifter * Unité de branchement | FPU |- | * Additions, soustractions, opérations bit à bit, etc. * Multiplication | * Additions, soustractions, opérations bit à bit, etc. * Décalages et rotations * Branchements |Opération flottante |} ===Les CPU superscalaires à plusieurs unités spécialisées=== L'évolution des CPU superscalaires s'est faite en quatre temps, et nous en sommes à la quatrième étape. Elle ne correspond en réalité pas à grand chose de concret, ni à une évolution technique bien précise. Elle correspond en réalité à l'arrivée de processeurs superscalaires très variés, qui n'ont pas grand chose en commun. Le premier CPU superscalaire grand public était le Pentium. Et c'était un CPU double émission qui n'implémentait pas la double émission entière-flottante, mais qui avait deux ALU entières. 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. Ils ont eux aussi privilégié l'ajout d'ALU entières. Les multiplieurs n'ont pas été dupliqués, sauf sur les processeurs superscalaires très larges. La raison est qu'il est rare de devoir exécuter deux multiplications à la fois. Et c'est la même chose pour le ''barrel shifter''. Pour donner quelques exemples, étudions les processeurs superscalaire Power PC. Il y a eu plusieurs modèles, avec des jeux d'instructions légèrement différents, mais aussi et surtout des microarchitectures assez différentes. Les premiers, avant le Power PC G4, étaient des CPU étroits, qui hésitaient entre 2, 3 et 4 voies. Par contre, à partir du G5, on passe à des CPU larges, avec 8 voies ! Le premier CPU PowerPC superscalaire était le PowerPC 601. Il avait une forme limitée de triple émission, identique à celle observée sur les CPU superscalaire historiques. Il pouvait émettre en même temps : une opération entière/mémoire, une opération flottante, et un branchement. Le PowerPC 603 utilisait une forme hybride entre double émission et émission parallèle des branchements. En clair, il pouvait émettre deux instructions + un branchement. Il intégrait 5 unités de calcul : une ALU entière, une FPU, l'unité de branchement, l'unité mémoire et une unité système pour gérer les registres de contrôle et les instructions particulières. Si on omet les branchements, il pouvait émettre une opération entière avec une autre instruction (non-entière, vu qu'il n'y a qu'une seule ALU). Il pouvait aussi émettre une instruction flottante et une µop mémoire simultanément. Il avait donc une réelle double émission, ce qui fait qu'il était qualifié de ''CPU 2 + Branch''. Le PowerPC 604 est lui passé ) la quadruple émission, avec émission parallèle des branchements. En clair, c'était un CPU de type ''4 + Branch''. Il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire, l'unité de branchement. La FPU et le multiplieur étaient sur le même port d'émission, mais c'était le seul à être partagé. En clair, le CPU pouvait émettre deux instructions entières, un accès mémoire, un branchement, et : soit une multiplication/division, soit une opération flottante. Le PowerPC 620 était similaire, sauf qu'il permettait d'émettre deux accès mémoire, ce qu'on n'a pas encore vu à ce stade du cours. Un exemple est celui du processeur superscalaire double émission Power PC 440. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. Une organisation simple, mais très efficace ! [[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]] La '''double émission flottante''' a été utilisée sur les processeurs Alpha. L'idée est d'émettre deux opérations flottantes à la fois. Sauf qu'une implémentation naïve demanderait de dupliquer la FPU, ce qui aurait un cout en circuit rédhibitoire. Pour rappel, les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant. L'idée est d'avoir des ports d'émission séparés pour l'additionneur flottant et le multiplieur flottant. Le processeur peut alors émettre une addition flottante en même temps qu'une multiplication flottante. Les autres circuits de calcul flottant sont répartis sur ces deux ports d'émission flottants. L'avantage est que cela se marie bien avec l'usage d'une fenêtre d'instruction séparée pour les opérations flottantes. La fenêtre d'instruction a alors deux ports séparés, au lieu d'un seul. Rajouter un second port d'émission flottant n'est pas trop un problème, car le cout lié à l'ajout d'un port n'est pas linéaire. Passer de un port à deux a un cout tolérable, bien plus que de passer de 3 ports à 4 ou de 4 à 5. Il est possible de dupliquer l'additionneur et le multiplieur flottant, ce qui permet d'émettre deux additions et multiplications flottantes en même temps. C'est ce qui est fait sur les processeurs AMD d'architecture Zen 1 et 2. Ils ont deux additionneurs flottants par cœur, deux multiplieurs flottants, chacun avec leur propre port d'émission. Les performances en calcul flottant sont assez impressionnantes pour un processeur de l'époque. [[File:ZEN - émission multiple flottante.png|centre|vignette|upright=2|Microarchitecture Zen 1 d'AMD.]] Pour le moment, nous n'avons vu que des cas où un port d'émission est relié soit à des ALU entières, soit à des FPU, pas aux deux. Mais il existe des processeurs où un même port d'émission alimente à la fois une ALU entière et une FPU. Par exemple, on peut relier un additionneur flottant sur le même port qu'une ALU entière. Il faut noter que cela implique une fenêtre d'instruction centralisée, capable de mettre en attente micro-opérations entières et flottantes. Un exemple est celui des processeurs Core 2 Duo. Ils disposent de 6 ports d'émission, dont 3 ports dédiés à l'unité mémoire. Les 3 ports restants alimentent chacun : une ALU entière, un circuit de calcul flottant et une unité de calcul SSE (une unité de calcul SIMD, qu'on abordera dans quelques chapitres). * Le premier port alimente une ALU entière simple et un multiplieur/diviseur flottant. * Le second alimente une ALU entière, un multiplieur entier et un additionneur flottant. * Le troisième alimente une ALU entière, sans circuit flottant dédié. ==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 simultanné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]] 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=== 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. Elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. La ''load/store queue'' reste une structure unique, mais on peut lui rajouter des ports. Et à ce peit jeu, il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire. [[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]] 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. Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre. Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Le Pentium 4 était dans ce cas, mais il était un peu seul dans sa catégorie. Un cas similaire est celui où l'unité mémoire est alimentée par une fenêtre de micro-opération mélangeant instructions mémoire et entières. Les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. Dans ce cas, l'émission multiple est rarement implémentée. Le cas suivant a une file pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique. Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui pouvaient seulement émettre une lecture en même temps qu'une écriture. Ils avaient deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse. [[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]] Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture demande d'utiliser une file d'écriture multiport, ce qui est assez compliqué. Implémenter des lectures en plus est plus simple : il suffit de rajouter une seconde file de µops LOAD et une unité de calcul, alimentées via un second port d'émission. Il est aussi possible de rajouter un second port de lecture au cache, mais ce n'est pas obligatoire. Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse. De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports de lecture et d'écriture car il y a plus de lecture que d'écritures. Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. : Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse. [[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]] Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettent plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail. ==Les optimisations des processeurs superscalaires larges== Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul. ===L'étape de chargement d'un CPU superscalaire large=== Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 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 autoaligné.]] Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements ! [[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]] Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez 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 microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total). Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois 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 microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement. Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations. ===La micro-fusion des processeurs Intel et AMD=== La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''. Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''. Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout. La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Le parallélisme mémoire | prevText=Le parallélisme mémoire | next=Exemples de microarchitectures CPU : le cas du x86 | nextText=Exemples de microarchitectures CPU : le cas du x86 }} </noinclude> pfwxyl0dyc9w808pj1a783wka2pz4ll 772741 772740 2026-09-21T20:30:54Z Mewtow 31375 /* Les CPU superscalaires à plusieurs unités spécialisées */ 772741 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. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps. 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. 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. Deux branchements consécutifs sont quant à eux plus fréquents, mais exécuter deux branchements en même temps pose un problème : que faire quand les deux sont pris ? Deux accès mémoire consécutifs est assez rare. La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs sont systématiquement supportées, car dupliquer des ALU 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 et 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'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. 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. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant. 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. ===Le chargement de plusieurs instructions consécutives=== Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large. Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence. Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas. Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement non-pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc. [[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]] ===Le décodage parallèle des instructions=== Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits. Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur. Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! ===L'unité d'émission d'un processeur superscalaire=== La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. 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 4 instructions, compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément deux 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. 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. : 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 microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle. 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. ==L'évolution historique des processeurs superscalaires== L'évolution des CPU superscalaires historiques 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. Leur historique distingue six étapes, mais je vais en omettre une et fusionner les deux dernières. Le résultat est une évolution en quatre temps, qui portent les noms de : * double émission entière-flottante ; * exécution parallèle des branchements ; * émission multiple des opérations entières. Les tout premiers processeurs superscalaires avaient un budget en transistors très limité, ce qui fait que les contraintes d'appariement étaient très strictes. Les deux premières étapes ne faisaient que mieux utiliser des unités de calcul déjà présentes, ils ne dupliquaient aucune unité de calcul. La troisième a ajouté des ALUs entières, pour améliorer un peu les performances. Après la quatrième étape, les CPU superscalaires sont devenus très variés, au point de ne pas pouvoir être classés simplement. ===La double émission entière-flottante=== Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est d'émettre une micro-opération entière en même temps qu'une micro-opé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, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité. La plupart des premiers processeurs superscalaires étaient de ce type. Les exemples les plus notables sont les processeurs POWER 1 et ses dérivés comme le ''RISC Single Chip''. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle. [[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]] La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisations visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate]. ===L'émission parallèle des branchements=== Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur. 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|Emission multiple des opérations entières, implémentation courante]] Pour les branchements, deux solutions sont possibles. Avec la première, il y a émission parallèle des branchements, à savoir qu'il est possible d'émettre en branchement en parallèle de deux-trois instructions entières. Et cela impose la présence d'une unité de branchement séparée, avec son port d'émission. Avec la seconde, les branchements sont traités comme des instructions entières. Soit les branchements sont exécutés dans une ALU entière, soit il y a une unité de branchement reliée à un port d'émission pour les opérations entières. [[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Emission série et parallèle des branchements]] En général, les branchements sont exécutés comme des opérations entières. Cela permet d'économiser une unité de calcul et beaucoup de transistors dans l'unité d'émission/chargement/... {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 |- | * ALU entière * Multiplieur | * ALU entière * Barrel shifter * Unité de branchement | FPU |- | * Additions, soustractions, opérations bit à bit, etc. * Multiplication | * Additions, soustractions, opérations bit à bit, etc. * Décalages et rotations * Branchements |Opération flottante |} ===Les CPU superscalaires à plusieurs unités spécialisées=== L'évolution des CPU superscalaires s'est faite en quatre temps, et nous en sommes à la quatrième étape. Elle ne correspond en réalité pas à grand chose de concret, ni à une évolution technique bien précise. Elle correspond en réalité à l'arrivée de processeurs superscalaires très variés, qui n'ont pas grand chose en commun. Le premier CPU superscalaire grand public était le Pentium. Et c'était un CPU double émission qui n'implémentait pas la double émission entière-flottante, mais qui avait deux ALU entières. 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. Ils ont eux aussi privilégié l'ajout d'ALU entières. Les multiplieurs n'ont pas été dupliqués, sauf sur les processeurs superscalaires très larges. La raison est qu'il est rare de devoir exécuter deux multiplications à la fois. Et c'est la même chose pour le ''barrel shifter''. Pour donner quelques exemples, étudions les processeurs superscalaire Power PC. Il y a eu plusieurs modèles, avec des jeux d'instructions légèrement différents, mais aussi et surtout des microarchitectures assez différentes. Les premiers, avant le Power PC G4, étaient des CPU étroits, qui hésitaient entre 2, 3 et 4 voies. Par contre, à partir du G5, on passe à des CPU larges, avec 8 voies ! Un exemple est celui du processeur superscalaire double émission Power PC 440, un CPU déstiné à l'embarqué. Il dispose de deux ports d'émission. Le premier 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.]] Mais il s'agit là d'un modèle pour l'embarqué. Voyons plutôt les modèles haute performance. Afin de simplifier les explications, 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 unemultiplication, éventuellement à une division. * FLOAT désigne une opération flottante, peut importe laquelle. * MEME désigne un accès mémoire, à savoir une instruction ou µop mémoire. * BRANCH désigne une instruction de branchement. Le premier était le PowerPC 601. Il avait une forme limitée de triple émission, identique à celle observée sur les CPU superscalaire historiques. Il pouvait émettre en même temps : une opération entière/mémoire, une opération flottante, et un branchement. En clair, INT + FLOAT + BRANCH Le PowerPC 603 utilisait une double émission "parfaite", avec émission parallèle des branchements. En clair, il pouvait émettre deux instructions + un branchement. Il intégrait une ALU entière, une FPU, l'unité de branchement et l'unité mémoire. Toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. Il avait donc une réelle double émission, ce qui fait qu'il était qualifié de ''CPU 2 + Branch''. Le PowerPC 604 est lui passé à la quadruple émission, avec émission parallèle des branchements. En clair, c'était un CPU de type ''4 + Branch''. Il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire, l'unité de branchement. La FPU et le multiplieur étaient sur le même port d'émission, mais c'était le seul à être partagé. En clair, le CPU pouvait émettre deux INT + MEM + BRANCH + (soit MUL, soit FLOAT). Vint ensuite la génération suivante avec les PowerPC G3 et G4. Ils implémentaient la double émission + branchement. La différence avec le 603 est qu'ils intégraient deux ALU entières, pas une seule. Ils pouvaient donc exécuter deux opérations entières en même temps, la seule contrainte étant sur les multiplications (il n'y a qu'un seul multiplieur). Les combinaisons possibles étaient INT + INT , INT + MUL, INT + FLOAT, INT + MEM, FLOAT + MEM. La '''double émission flottante''' a été utilisée sur les processeurs Alpha. L'idée est d'émettre deux opérations flottantes à la fois. Sauf qu'une implémentation naïve demanderait de dupliquer la FPU, ce qui aurait un cout en circuit rédhibitoire. Pour rappel, les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant. L'idée est d'avoir des ports d'émission séparés pour l'additionneur flottant et le multiplieur flottant. Le processeur peut alors émettre une addition flottante en même temps qu'une multiplication flottante. Les autres circuits de calcul flottant sont répartis sur ces deux ports d'émission flottants. L'avantage est que cela se marie bien avec l'usage d'une fenêtre d'instruction séparée pour les opérations flottantes. La fenêtre d'instruction a alors deux ports séparés, au lieu d'un seul. Rajouter un second port d'émission flottant n'est pas trop un problème, car le cout lié à l'ajout d'un port n'est pas linéaire. Passer de un port à deux a un cout tolérable, bien plus que de passer de 3 ports à 4 ou de 4 à 5. Il est possible de dupliquer l'additionneur et le multiplieur flottant, ce qui permet d'émettre deux additions et multiplications flottantes en même temps. C'est ce qui est fait sur les processeurs AMD d'architecture Zen 1 et 2. Ils ont deux additionneurs flottants par cœur, deux multiplieurs flottants, chacun avec leur propre port d'émission. Les performances en calcul flottant sont assez impressionnantes pour un processeur de l'époque. [[File:ZEN - émission multiple flottante.png|centre|vignette|upright=2|Microarchitecture Zen 1 d'AMD.]] Pour le moment, nous n'avons vu que des cas où un port d'émission est relié soit à des ALU entières, soit à des FPU, pas aux deux. Mais il existe des processeurs où un même port d'émission alimente à la fois une ALU entière et une FPU. Par exemple, on peut relier un additionneur flottant sur le même port qu'une ALU entière. Il faut noter que cela implique une fenêtre d'instruction centralisée, capable de mettre en attente micro-opérations entières et flottantes. Un exemple est celui des processeurs Core 2 Duo. Ils disposent de 6 ports d'émission, dont 3 ports dédiés à l'unité mémoire. Les 3 ports restants alimentent chacun : une ALU entière, un circuit de calcul flottant et une unité de calcul SSE (une unité de calcul SIMD, qu'on abordera dans quelques chapitres). * Le premier port alimente une ALU entière simple et un multiplieur/diviseur flottant. * Le second alimente une ALU entière, un multiplieur entier et un additionneur flottant. * Le troisième alimente une ALU entière, sans circuit flottant dédié. ==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 simultanné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]] 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=== 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. Elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. La ''load/store queue'' reste une structure unique, mais on peut lui rajouter des ports. Et à ce peit jeu, il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire. [[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]] 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. Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre. Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Le Pentium 4 était dans ce cas, mais il était un peu seul dans sa catégorie. Un cas similaire est celui où l'unité mémoire est alimentée par une fenêtre de micro-opération mélangeant instructions mémoire et entières. Les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. Dans ce cas, l'émission multiple est rarement implémentée. Le cas suivant a une file pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique. Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui pouvaient seulement émettre une lecture en même temps qu'une écriture. Ils avaient deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse. [[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]] Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture demande d'utiliser une file d'écriture multiport, ce qui est assez compliqué. Implémenter des lectures en plus est plus simple : il suffit de rajouter une seconde file de µops LOAD et une unité de calcul, alimentées via un second port d'émission. Il est aussi possible de rajouter un second port de lecture au cache, mais ce n'est pas obligatoire. Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse. De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports de lecture et d'écriture car il y a plus de lecture que d'écritures. Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. : Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse. [[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]] Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettent plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail. ==Les optimisations des processeurs superscalaires larges== Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul. ===L'étape de chargement d'un CPU superscalaire large=== Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 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 autoaligné.]] Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements ! [[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]] Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez 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 microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total). Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois 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 microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement. Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations. ===La micro-fusion des processeurs Intel et AMD=== La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''. Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''. Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout. La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Le parallélisme mémoire | prevText=Le parallélisme mémoire | next=Exemples de microarchitectures CPU : le cas du x86 | nextText=Exemples de microarchitectures CPU : le cas du x86 }} </noinclude> 9uxux7kfa5ccbx9qpt1iyiqoo0d78v8 772742 772741 2026-09-21T20:39:50Z Mewtow 31375 /* Les CPU superscalaires à plusieurs unités spécialisées */ 772742 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. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps. 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. 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. Deux branchements consécutifs sont quant à eux plus fréquents, mais exécuter deux branchements en même temps pose un problème : que faire quand les deux sont pris ? Deux accès mémoire consécutifs est assez rare. La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs sont systématiquement supportées, car dupliquer des ALU 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 et 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'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. 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. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant. 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. ===Le chargement de plusieurs instructions consécutives=== Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large. Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence. Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas. Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement non-pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc. [[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]] ===Le décodage parallèle des instructions=== Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits. Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur. Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! ===L'unité d'émission d'un processeur superscalaire=== La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. 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 4 instructions, compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément deux 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. 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. : 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 microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle. 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. ==L'évolution historique des processeurs superscalaires== L'évolution des CPU superscalaires historiques 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. Leur historique distingue six étapes, mais je vais en omettre une et fusionner les deux dernières. Le résultat est une évolution en quatre temps, qui portent les noms de : * double émission entière-flottante ; * exécution parallèle des branchements ; * émission multiple des opérations entières. Les tout premiers processeurs superscalaires avaient un budget en transistors très limité, ce qui fait que les contraintes d'appariement étaient très strictes. Les deux premières étapes ne faisaient que mieux utiliser des unités de calcul déjà présentes, ils ne dupliquaient aucune unité de calcul. La troisième a ajouté des ALUs entières, pour améliorer un peu les performances. Après la quatrième étape, les CPU superscalaires sont devenus très variés, au point de ne pas pouvoir être classés simplement. ===La double émission entière-flottante=== Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est d'émettre une micro-opération entière en même temps qu'une micro-opé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, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité. La plupart des premiers processeurs superscalaires étaient de ce type. Les exemples les plus notables sont les processeurs POWER 1 et ses dérivés comme le ''RISC Single Chip''. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle. [[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]] La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisations visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate]. ===L'émission parallèle des branchements=== Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur. 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|Emission multiple des opérations entières, implémentation courante]] Pour les branchements, deux solutions sont possibles. Avec la première, il y a émission parallèle des branchements, à savoir qu'il est possible d'émettre en branchement en parallèle de deux-trois instructions entières. Et cela impose la présence d'une unité de branchement séparée, avec son port d'émission. Avec la seconde, les branchements sont traités comme des instructions entières. Soit les branchements sont exécutés dans une ALU entière, soit il y a une unité de branchement reliée à un port d'émission pour les opérations entières. [[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Emission série et parallèle des branchements]] En général, les branchements sont exécutés comme des opérations entières. Cela permet d'économiser une unité de calcul et beaucoup de transistors dans l'unité d'émission/chargement/... {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 |- | * ALU entière * Multiplieur | * ALU entière * Barrel shifter * Unité de branchement | FPU |- | * Additions, soustractions, opérations bit à bit, etc. * Multiplication | * Additions, soustractions, opérations bit à bit, etc. * Décalages et rotations * Branchements |Opération flottante |} ===Les CPU superscalaires à plusieurs unités spécialisées=== L'évolution des CPU superscalaires s'est faite en quatre temps, et nous en sommes à la quatrième étape. Elle ne correspond en réalité pas à grand chose de concret, ni à une évolution technique bien précise. Elle correspond en réalité à l'arrivée de processeurs superscalaires très variés, qui n'ont pas grand chose en commun. Le premier CPU superscalaire grand public était le Pentium. Et c'était un CPU double émission qui n'implémentait pas la double émission entière-flottante, mais qui avait deux ALU entières. 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. Ils ont eux aussi privilégié l'ajout d'ALU entières. Les multiplieurs n'ont pas été dupliqués, sauf sur les processeurs superscalaires très larges. La raison est qu'il est rare de devoir exécuter deux multiplications à la fois. Et c'est la même chose pour le ''barrel shifter''. Un exemple de CPU double émission 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.]] Pour donner quelques exemples, étudions les processeurs superscalaire PowerPC. Il y a eu plusieurs modèles, avec des jeux d'instructions légèrement différents, mais aussi et surtout des microarchitectures assez différentes. Afin de simplifier les explications, 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 unemultiplication, éventuellement à une division. * FLOAT désigne une opération flottante, peut importe laquelle. * MEME désigne un accès mémoire, à savoir une instruction ou µop mémoire. * BRANCH désigne une instruction de branchement. Le PowerPC 603 utilisait une double émission "parfaite", avec émission parallèle des branchements. En clair, il pouvait émettre deux instructions + un branchement. Il intégrait une ALU entière, une FPU, l'unité de branchement et l'unité mémoire. Toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. Il avait donc une réelle double émission, ce qui fait qu'il était qualifié de ''CPU 2 + Branch''. Le PowerPC 604 est lui passé à la quadruple émission, avec émission parallèle des branchements. En clair, c'était un CPU de type ''4 + Branch''. 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, mais c'était le seul à être partagé. En clair, le CPU pouvait émettre deux INT + MEM + (soit MUL, soit FLOAT). Vint ensuite la génération suivante avec les PowerPC G3 et G4. Ils implémentaient la double émission + branchement. La différence avec le 603 est qu'ils intégraient deux ALU entières, pas une seule. Ils pouvaient donc exécuter deux opérations entières en même temps, la seule contrainte étant sur les multiplications (il n'y a qu'un seul multiplieur). Les combinaisons possibles étaient INT + INT , INT + MUL, INT + FLOAT, INT + MEM, FLOAT + MEM. La '''double émission flottante''' a été utilisée sur les processeurs Alpha et SPARC. L'idée est d'émettre deux opérations flottantes à la fois. Sauf qu'une implémentation naïve demanderait de dupliquer la FPU, ce qui aurait un cout en circuit rédhibitoire. Pour rappel, les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant. L'idée est d'avoir des ports d'émission séparés pour l'additionneur flottant et le multiplieur flottant. Le processeur peut alors émettre une addition flottante en même temps qu'une multiplication flottante. Les autres circuits de calcul flottant sont répartis sur ces deux ports d'émission flottants. L'avantage est que cela se marie bien avec l'usage d'une fenêtre d'instruction séparée pour les opérations flottantes. La fenêtre d'instruction a alors deux ports séparés, au lieu d'un seul. Rajouter un second port d'émission flottant n'est pas trop un problème, car le cout lié à l'ajout d'un port n'est pas linéaire. Passer de un port à deux a un cout tolérable, bien plus que de passer de 3 ports à 4 ou de 4 à 5. Il est possible de dupliquer l'additionneur et le multiplieur flottant, ce qui permet d'émettre deux additions et multiplications flottantes en même temps. C'est ce qui est fait sur les processeurs AMD d'architecture Zen 1 et 2. Ils ont deux additionneurs flottants par cœur, deux multiplieurs flottants, chacun avec leur propre port d'émission. Les performances en calcul flottant sont assez impressionnantes pour un processeur de l'époque. [[File:ZEN - émission multiple flottante.png|centre|vignette|upright=2|Microarchitecture Zen 1 d'AMD.]] Il existe des processeurs où un même port d'émission alimente à la fois une ALU entière et une FPU. Par exemple, on peut relier un additionneur flottant sur le même port qu'une ALU entière. Il faut noter que cela implique une fenêtre d'instruction centralisée, capable de mettre en attente micro-opérations entières et flottantes. Un exemple est celui des processeurs Core 2 Duo. Ils disposent de 6 ports d'émission, dont 3 ports dédiés à l'unité mémoire. Les 3 ports restants alimentent chacun : une ALU entière, un circuit de calcul flottant et une unité de calcul SIMD (on abordera le SIMD dans quelques chapitres). * Le premier port alimente une ALU entière simple et un multiplieur/diviseur flottant. * Le second alimente une ALU entière, un multiplieur entier et un additionneur flottant. * Le troisième alimente une ALU entière, sans circuit flottant dédié. ==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 simultanné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]] 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=== 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. Elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. La ''load/store queue'' reste une structure unique, mais on peut lui rajouter des ports. Et à ce peit jeu, il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire. [[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]] 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. Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre. Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Le Pentium 4 était dans ce cas, mais il était un peu seul dans sa catégorie. Un cas similaire est celui où l'unité mémoire est alimentée par une fenêtre de micro-opération mélangeant instructions mémoire et entières. Les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. Dans ce cas, l'émission multiple est rarement implémentée. Le cas suivant a une file pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique. Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui pouvaient seulement émettre une lecture en même temps qu'une écriture. Ils avaient deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse. [[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]] Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture demande d'utiliser une file d'écriture multiport, ce qui est assez compliqué. Implémenter des lectures en plus est plus simple : il suffit de rajouter une seconde file de µops LOAD et une unité de calcul, alimentées via un second port d'émission. Il est aussi possible de rajouter un second port de lecture au cache, mais ce n'est pas obligatoire. Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse. De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports de lecture et d'écriture car il y a plus de lecture que d'écritures. Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. : Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse. [[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]] Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettent plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail. ==Les optimisations des processeurs superscalaires larges== Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul. ===L'étape de chargement d'un CPU superscalaire large=== Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 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 autoaligné.]] Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements ! [[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]] Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez 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 microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total). Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois 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 microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement. Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations. ===La micro-fusion des processeurs Intel et AMD=== La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''. Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''. Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout. La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Le parallélisme mémoire | prevText=Le parallélisme mémoire | next=Exemples de microarchitectures CPU : le cas du x86 | nextText=Exemples de microarchitectures CPU : le cas du x86 }} </noinclude> sec6nlpc9s889zggh73d2jv7wqfzaep 772743 772742 2026-09-21T20:44:31Z Mewtow 31375 /* Les CPU superscalaires à plusieurs unités spécialisées */ 772743 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. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps. 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. 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. Deux branchements consécutifs sont quant à eux plus fréquents, mais exécuter deux branchements en même temps pose un problème : que faire quand les deux sont pris ? Deux accès mémoire consécutifs est assez rare. La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs sont systématiquement supportées, car dupliquer des ALU 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 et 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'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. 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. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant. 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. ===Le chargement de plusieurs instructions consécutives=== Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large. Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence. Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas. Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement non-pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc. [[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]] ===Le décodage parallèle des instructions=== Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits. Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur. Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! ===L'unité d'émission d'un processeur superscalaire=== La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. 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 4 instructions, compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément deux 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. 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. : 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 microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle. 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. ==L'évolution historique des processeurs superscalaires== L'évolution des CPU superscalaires historiques 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. Leur historique distingue six étapes, mais je vais en omettre une et fusionner les deux dernières. Le résultat est une évolution en quatre temps, qui portent les noms de : * double émission entière-flottante ; * exécution parallèle des branchements ; * émission multiple des opérations entières. Les tout premiers processeurs superscalaires avaient un budget en transistors très limité, ce qui fait que les contraintes d'appariement étaient très strictes. Les deux premières étapes ne faisaient que mieux utiliser des unités de calcul déjà présentes, ils ne dupliquaient aucune unité de calcul. La troisième a ajouté des ALUs entières, pour améliorer un peu les performances. Après la quatrième étape, les CPU superscalaires sont devenus très variés, au point de ne pas pouvoir être classés simplement. ===La double émission entière-flottante=== Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est d'émettre une micro-opération entière en même temps qu'une micro-opé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, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité. La plupart des premiers processeurs superscalaires étaient de ce type. Les exemples les plus notables sont les processeurs POWER 1 et ses dérivés comme le ''RISC Single Chip''. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle. [[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]] La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisations visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate]. ===L'émission parallèle des branchements=== Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur. 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|Emission multiple des opérations entières, implémentation courante]] Pour les branchements, deux solutions sont possibles. Avec la première, il y a émission parallèle des branchements, à savoir qu'il est possible d'émettre en branchement en parallèle de deux-trois instructions entières. Et cela impose la présence d'une unité de branchement séparée, avec son port d'émission. Avec la seconde, les branchements sont traités comme des instructions entières. Soit les branchements sont exécutés dans une ALU entière, soit il y a une unité de branchement reliée à un port d'émission pour les opérations entières. [[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Emission série et parallèle des branchements]] En général, les branchements sont exécutés comme des opérations entières. Cela permet d'économiser une unité de calcul et beaucoup de transistors dans l'unité d'émission/chargement/... {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 |- | * ALU entière * Multiplieur | * ALU entière * Barrel shifter * Unité de branchement | FPU |- | * Additions, soustractions, opérations bit à bit, etc. * Multiplication | * Additions, soustractions, opérations bit à bit, etc. * Décalages et rotations * Branchements |Opération flottante |} ===Les CPU superscalaires à plusieurs unités spécialisées=== L'évolution des CPU superscalaires s'est faite en quatre temps, et nous en sommes à la quatrième étape. Elle ne correspond en réalité pas à grand chose de concret, ni à une évolution technique bien précise. Elle correspond en réalité à l'arrivée de processeurs superscalaires très variés, qui n'ont pas grand chose en commun. Le premier CPU superscalaire grand public était le Pentium. Et c'était un CPU double émission qui n'implémentait pas la double émission entière-flottante, mais qui avait deux ALU entières. 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. Ils ont eux aussi privilégié l'ajout d'ALU entières. Les multiplieurs n'ont pas été dupliqués, sauf sur les processeurs superscalaires très larges. La raison est qu'il est rare de devoir exécuter deux multiplications à la fois. Et c'est la même chose pour le ''barrel shifter''. Pour donner quelques exemples, étudions les processeurs superscalaire PowerPC. Il y a eu plusieurs modèles, avec des jeux d'instructions légèrement différents, mais aussi et surtout des microarchitectures assez différentes. Afin de simplifier les explications, 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 unemultiplication, éventuellement à une division. * FLOAT désigne une opération flottante, peut importe laquelle. * MEME désigne un accès mémoire, à savoir une instruction ou µop mémoire. * BRANCH désigne une instruction de branchement. Le PowerPC 603 utilisait une double émission "parfaite", avec émission parallèle des branchements. En clair, il pouvait émettre deux instructions + un branchement. Il intégrait une ALU entière, une FPU, l'unité de branchement et l'unité mémoire. Toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. Il avait donc une réelle double émission, ce qui fait qu'il était qualifié de ''CPU 2 + Branch''. Le PowerPC 604 est lui passé à la quadruple émission, avec émission parallèle des branchements. En clair, c'était un CPU de type ''4 + Branch''. 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, mais c'était le seul à être partagé. En clair, le CPU pouvait émettre deux INT + MEM + (soit MUL, soit FLOAT). Vint ensuite la génération suivante avec les PowerPC G3 et G4. Ils implémentaient la double émission + branchement. La différence avec le 603 est qu'ils intégraient deux ALU entières, pas une seule. Ils pouvaient donc exécuter deux opérations entières en même temps, la seule contrainte étant sur les multiplications (il n'y a qu'un seul multiplieur). Les combinaisons possibles étaient INT + INT , INT + MUL, INT + FLOAT, INT + MEM, FLOAT + MEM. La '''double émission flottante''' a été utilisée sur les processeurs Alpha et SPARC. L'idée est d'émettre deux opérations flottantes à la fois. Sauf qu'une implémentation naïve demanderait de dupliquer la FPU, ce qui aurait un cout en circuit rédhibitoire. Pour rappel, les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant. L'idée est d'avoir des ports d'émission séparés pour l'additionneur flottant et le multiplieur flottant. Le processeur peut alors émettre une addition flottante en même temps qu'une multiplication flottante. Les autres circuits de calcul flottant sont répartis sur ces deux ports d'émission flottants. L'avantage est que cela se marie bien avec l'usage d'une fenêtre d'instruction séparée pour les opérations flottantes. La fenêtre d'instruction a alors deux ports séparés, au lieu d'un seul. Rajouter un second port d'émission flottant n'est pas trop un problème, car le cout lié à l'ajout d'un port n'est pas linéaire. Passer de un port à deux a un cout tolérable, bien plus que de passer de 3 ports à 4 ou de 4 à 5. Il est possible de dupliquer l'additionneur et le multiplieur flottant, ce qui permet d'émettre deux additions et multiplications flottantes en même temps. C'est ce qui est fait sur les processeurs AMD d'architecture Zen 1 et 2. Ils ont deux additionneurs flottants par cœur, deux multiplieurs flottants, chacun avec leur propre port d'émission. Les performances en calcul flottant sont assez impressionnantes pour un processeur de l'époque. [[File:ZEN - émission multiple flottante.png|centre|vignette|upright=2|Microarchitecture Zen 1 d'AMD.]] Il existe des processeurs où un même port d'émission alimente à la fois une ALU entière et une FPU. Par exemple, on peut relier un additionneur flottant sur le même port qu'une ALU entière. Il faut noter que cela implique une fenêtre d'instruction centralisée, capable de mettre en attente micro-opérations entières et flottantes. Un exemple est celui des processeurs Core 2 Duo. Ils disposent de 6 ports d'émission, dont 3 ports dédiés à l'unité mémoire. Les 3 ports restants alimentent chacun : une ALU entière, un circuit de calcul flottant et une unité de calcul SIMD (on abordera le SIMD dans quelques chapitres). * Le premier port alimente une ALU entière simple et un multiplieur/diviseur flottant. * Le second alimente une ALU entière, un multiplieur entier et un additionneur flottant. * Le troisième alimente une ALU entière, sans circuit flottant dédié. ==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 simultanné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]] 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=== 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. Elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. La ''load/store queue'' reste une structure unique, mais on peut lui rajouter des ports. Et à ce peit jeu, il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire. [[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]] 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. Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre. Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Le Pentium 4 était dans ce cas, mais il était un peu seul dans sa catégorie. Un cas similaire est celui où l'unité mémoire est alimentée par une fenêtre de micro-opération mélangeant instructions mémoire et entières. Les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. Dans ce cas, l'émission multiple est rarement implémentée. Le cas suivant a une file pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique. Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui pouvaient seulement émettre une lecture en même temps qu'une écriture. Ils avaient deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse. [[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]] Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture demande d'utiliser une file d'écriture multiport, ce qui est assez compliqué. Implémenter des lectures en plus est plus simple : il suffit de rajouter une seconde file de µops LOAD et une unité de calcul, alimentées via un second port d'émission. Il est aussi possible de rajouter un second port de lecture au cache, mais ce n'est pas obligatoire. Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse. De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports de lecture et d'écriture car il y a plus de lecture que d'écritures. Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. : Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse. [[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]] Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettent plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail. ==Les optimisations des processeurs superscalaires larges== Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul. ===L'étape de chargement d'un CPU superscalaire large=== Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 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 autoaligné.]] Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements ! [[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]] Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez 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 microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total). Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois 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 microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement. Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations. ===La micro-fusion des processeurs Intel et AMD=== La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''. Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''. Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout. La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Le parallélisme mémoire | prevText=Le parallélisme mémoire | next=Exemples de microarchitectures CPU : le cas du x86 | nextText=Exemples de microarchitectures CPU : le cas du x86 }} </noinclude> 1xql1ldveu4qwy15ug0p28qzenh32c9 772744 772743 2026-09-21T20:45:06Z Mewtow 31375 /* Les accès mémoire traités dans le pipeline entier */ 772744 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. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps. 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. 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. Deux branchements consécutifs sont quant à eux plus fréquents, mais exécuter deux branchements en même temps pose un problème : que faire quand les deux sont pris ? Deux accès mémoire consécutifs est assez rare. La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs sont systématiquement supportées, car dupliquer des ALU 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 et 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'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. 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. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant. 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. ===Le chargement de plusieurs instructions consécutives=== Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large. Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence. Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas. Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement non-pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc. [[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]] ===Le décodage parallèle des instructions=== Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits. Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur. Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! ===L'unité d'émission d'un processeur superscalaire=== La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. 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 4 instructions, compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément deux 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. 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. : 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 microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle. 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. ==L'évolution historique des processeurs superscalaires== L'évolution des CPU superscalaires historiques 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. Leur historique distingue six étapes, mais je vais en omettre une et fusionner les deux dernières. Le résultat est une évolution en quatre temps, qui portent les noms de : * double émission entière-flottante ; * exécution parallèle des branchements ; * émission multiple des opérations entières. Les tout premiers processeurs superscalaires avaient un budget en transistors très limité, ce qui fait que les contraintes d'appariement étaient très strictes. Les deux premières étapes ne faisaient que mieux utiliser des unités de calcul déjà présentes, ils ne dupliquaient aucune unité de calcul. La troisième a ajouté des ALUs entières, pour améliorer un peu les performances. Après la quatrième étape, les CPU superscalaires sont devenus très variés, au point de ne pas pouvoir être classés simplement. ===La double émission entière-flottante=== Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est d'émettre une micro-opération entière en même temps qu'une micro-opé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, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité. La plupart des premiers processeurs superscalaires étaient de ce type. Les exemples les plus notables sont les processeurs POWER 1 et ses dérivés comme le ''RISC Single Chip''. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle. [[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]] La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisations visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate]. ===L'émission parallèle des branchements=== Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur. 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|Emission multiple des opérations entières, implémentation courante]] Pour les branchements, deux solutions sont possibles. Avec la première, il y a émission parallèle des branchements, à savoir qu'il est possible d'émettre en branchement en parallèle de deux-trois instructions entières. Et cela impose la présence d'une unité de branchement séparée, avec son port d'émission. Avec la seconde, les branchements sont traités comme des instructions entières. Soit les branchements sont exécutés dans une ALU entière, soit il y a une unité de branchement reliée à un port d'émission pour les opérations entières. [[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Emission série et parallèle des branchements]] En général, les branchements sont exécutés comme des opérations entières. Cela permet d'économiser une unité de calcul et beaucoup de transistors dans l'unité d'émission/chargement/... {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 |- | * ALU entière * Multiplieur | * ALU entière * Barrel shifter * Unité de branchement | FPU |- | * Additions, soustractions, opérations bit à bit, etc. * Multiplication | * Additions, soustractions, opérations bit à bit, etc. * Décalages et rotations * Branchements |Opération flottante |} ===Les CPU superscalaires à plusieurs unités spécialisées=== L'évolution des CPU superscalaires s'est faite en quatre temps, et nous en sommes à la quatrième étape. Elle ne correspond en réalité pas à grand chose de concret, ni à une évolution technique bien précise. Elle correspond en réalité à l'arrivée de processeurs superscalaires très variés, qui n'ont pas grand chose en commun. Le premier CPU superscalaire grand public était le Pentium. Et c'était un CPU double émission qui n'implémentait pas la double émission entière-flottante, mais qui avait deux ALU entières. 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. Ils ont eux aussi privilégié l'ajout d'ALU entières. Les multiplieurs n'ont pas été dupliqués, sauf sur les processeurs superscalaires très larges. La raison est qu'il est rare de devoir exécuter deux multiplications à la fois. Et c'est la même chose pour le ''barrel shifter''. Pour donner quelques exemples, étudions les processeurs superscalaire PowerPC. Il y a eu plusieurs modèles, avec des jeux d'instructions légèrement différents, mais aussi et surtout des microarchitectures assez différentes. Afin de simplifier les explications, 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 unemultiplication, éventuellement à une division. * FLOAT désigne une opération flottante, peut importe laquelle. * MEME désigne un accès mémoire, à savoir une instruction ou µop mémoire. * BRANCH désigne une instruction de branchement. Le PowerPC 603 utilisait une double émission "parfaite", avec émission parallèle des branchements. En clair, il pouvait émettre deux instructions + un branchement. Il intégrait une ALU entière, une FPU, l'unité de branchement et l'unité mémoire. Toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. Il avait donc une réelle double émission, ce qui fait qu'il était qualifié de ''CPU 2 + Branch''. Le PowerPC 604 est lui passé à la quadruple émission, avec émission parallèle des branchements. En clair, c'était un CPU de type ''4 + Branch''. 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, mais c'était le seul à être partagé. En clair, le CPU pouvait émettre deux INT + MEM + (soit MUL, soit FLOAT). Vint ensuite la génération suivante avec les PowerPC G3 et G4. Ils implémentaient la double émission + branchement. La différence avec le 603 est qu'ils intégraient deux ALU entières, pas une seule. Ils pouvaient donc exécuter deux opérations entières en même temps, la seule contrainte étant sur les multiplications (il n'y a qu'un seul multiplieur). Les combinaisons possibles étaient INT + INT , INT + MUL, INT + FLOAT, INT + MEM, FLOAT + MEM. La '''double émission flottante''' a été utilisée sur les processeurs Alpha et SPARC. L'idée est d'émettre deux opérations flottantes à la fois. Sauf qu'une implémentation naïve demanderait de dupliquer la FPU, ce qui aurait un cout en circuit rédhibitoire. Pour rappel, les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant. L'idée est d'avoir des ports d'émission séparés pour l'additionneur flottant et le multiplieur flottant. Le processeur peut alors émettre une addition flottante en même temps qu'une multiplication flottante. Les autres circuits de calcul flottant sont répartis sur ces deux ports d'émission flottants. L'avantage est que cela se marie bien avec l'usage d'une fenêtre d'instruction séparée pour les opérations flottantes. La fenêtre d'instruction a alors deux ports séparés, au lieu d'un seul. Rajouter un second port d'émission flottant n'est pas trop un problème, car le cout lié à l'ajout d'un port n'est pas linéaire. Passer de un port à deux a un cout tolérable, bien plus que de passer de 3 ports à 4 ou de 4 à 5. Il est possible de dupliquer l'additionneur et le multiplieur flottant, ce qui permet d'émettre deux additions et multiplications flottantes en même temps. C'est ce qui est fait sur les processeurs AMD d'architecture Zen 1 et 2. Ils ont deux additionneurs flottants par cœur, deux multiplieurs flottants, chacun avec leur propre port d'émission. Les performances en calcul flottant sont assez impressionnantes pour un processeur de l'époque. [[File:ZEN - émission multiple flottante.png|centre|vignette|upright=2|Microarchitecture Zen 1 d'AMD.]] Il existe des processeurs où un même port d'émission alimente à la fois une ALU entière et une FPU. Par exemple, on peut relier un additionneur flottant sur le même port qu'une ALU entière. Il faut noter que cela implique une fenêtre d'instruction centralisée, capable de mettre en attente micro-opérations entières et flottantes. Un exemple est celui des processeurs Core 2 Duo. Ils disposent de 6 ports d'émission, dont 3 ports dédiés à l'unité mémoire. Les 3 ports restants alimentent chacun : une ALU entière, un circuit de calcul flottant et une unité de calcul SIMD (on abordera le SIMD dans quelques chapitres). * Le premier port alimente une ALU entière simple et un multiplieur/diviseur flottant. * Le second alimente une ALU entière, un multiplieur entier et un additionneur flottant. * Le troisième alimente une ALU entière, sans circuit flottant dédié. ==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 simultanné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=== 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. Elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. La ''load/store queue'' reste une structure unique, mais on peut lui rajouter des ports. Et à ce peit jeu, il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire. [[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]] 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. Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre. Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Le Pentium 4 était dans ce cas, mais il était un peu seul dans sa catégorie. Un cas similaire est celui où l'unité mémoire est alimentée par une fenêtre de micro-opération mélangeant instructions mémoire et entières. Les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. Dans ce cas, l'émission multiple est rarement implémentée. Le cas suivant a une file pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique. Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui pouvaient seulement émettre une lecture en même temps qu'une écriture. Ils avaient deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse. [[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]] Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture demande d'utiliser une file d'écriture multiport, ce qui est assez compliqué. Implémenter des lectures en plus est plus simple : il suffit de rajouter une seconde file de µops LOAD et une unité de calcul, alimentées via un second port d'émission. Il est aussi possible de rajouter un second port de lecture au cache, mais ce n'est pas obligatoire. Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse. De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports de lecture et d'écriture car il y a plus de lecture que d'écritures. Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. : Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse. [[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]] Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettent plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail. ==Les optimisations des processeurs superscalaires larges== Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul. ===L'étape de chargement d'un CPU superscalaire large=== Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 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 autoaligné.]] Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements ! [[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]] Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez 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 microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total). Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois 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 microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement. Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations. ===La micro-fusion des processeurs Intel et AMD=== La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''. Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''. Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout. La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Le parallélisme mémoire | prevText=Le parallélisme mémoire | next=Exemples de microarchitectures CPU : le cas du x86 | nextText=Exemples de microarchitectures CPU : le cas du x86 }} </noinclude> 5c2zcgydgppazpcolcbbndaarwflm9d 772745 772744 2026-09-21T20:45:43Z Mewtow 31375 /* Les CPU superscalaires à plusieurs unités spécialisées */ 772745 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. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps. 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. 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. Deux branchements consécutifs sont quant à eux plus fréquents, mais exécuter deux branchements en même temps pose un problème : que faire quand les deux sont pris ? Deux accès mémoire consécutifs est assez rare. La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs sont systématiquement supportées, car dupliquer des ALU 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 et 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'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. 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. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant. 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. ===Le chargement de plusieurs instructions consécutives=== Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large. Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence. Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas. Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement non-pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc. [[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]] ===Le décodage parallèle des instructions=== Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits. Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur. Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! ===L'unité d'émission d'un processeur superscalaire=== La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. 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 4 instructions, compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément deux 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. 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. : 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 microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle. 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. ==L'évolution historique des processeurs superscalaires== L'évolution des CPU superscalaires historiques 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. Leur historique distingue six étapes, mais je vais en omettre une et fusionner les deux dernières. Le résultat est une évolution en quatre temps, qui portent les noms de : * double émission entière-flottante ; * exécution parallèle des branchements ; * émission multiple des opérations entières. Les tout premiers processeurs superscalaires avaient un budget en transistors très limité, ce qui fait que les contraintes d'appariement étaient très strictes. Les deux premières étapes ne faisaient que mieux utiliser des unités de calcul déjà présentes, ils ne dupliquaient aucune unité de calcul. La troisième a ajouté des ALUs entières, pour améliorer un peu les performances. Après la quatrième étape, les CPU superscalaires sont devenus très variés, au point de ne pas pouvoir être classés simplement. ===La double émission entière-flottante=== Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est d'émettre une micro-opération entière en même temps qu'une micro-opé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, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité. La plupart des premiers processeurs superscalaires étaient de ce type. Les exemples les plus notables sont les processeurs POWER 1 et ses dérivés comme le ''RISC Single Chip''. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle. [[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]] La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisations visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate]. ===L'émission parallèle des branchements=== Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur. 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|Emission multiple des opérations entières, implémentation courante]] Pour les branchements, deux solutions sont possibles. Avec la première, il y a émission parallèle des branchements, à savoir qu'il est possible d'émettre en branchement en parallèle de deux-trois instructions entières. Et cela impose la présence d'une unité de branchement séparée, avec son port d'émission. Avec la seconde, les branchements sont traités comme des instructions entières. Soit les branchements sont exécutés dans une ALU entière, soit il y a une unité de branchement reliée à un port d'émission pour les opérations entières. [[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Emission série et parallèle des branchements]] En général, les branchements sont exécutés comme des opérations entières. Cela permet d'économiser une unité de calcul et beaucoup de transistors dans l'unité d'émission/chargement/... {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 |- | * ALU entière * Multiplieur | * ALU entière * Barrel shifter * Unité de branchement | FPU |- | * Additions, soustractions, opérations bit à bit, etc. * Multiplication | * Additions, soustractions, opérations bit à bit, etc. * Décalages et rotations * Branchements |Opération flottante |} ===Les CPU superscalaires à plusieurs unités spécialisées=== L'évolution des CPU superscalaires s'est faite en quatre temps, et nous en sommes à la quatrième étape. Elle ne correspond en réalité pas à grand chose de concret, ni à une évolution technique bien précise. Elle correspond en réalité à l'arrivée de processeurs superscalaires très variés, qui n'ont pas grand chose en commun. Le premier CPU superscalaire grand public était le Pentium. Et c'était un CPU double émission qui n'implémentait pas la double émission entière-flottante, mais qui avait deux ALU entières. 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. Ils ont eux aussi privilégié l'ajout d'ALU entières. Les multiplieurs n'ont pas été dupliqués, sauf sur les processeurs superscalaires très larges. La raison est qu'il est rare de devoir exécuter deux multiplications à la fois. Et c'est la même chose pour le ''barrel shifter''. Pour donner quelques exemples, étudions les processeurs superscalaire PowerPC. Il y a eu plusieurs modèles, avec des jeux d'instructions légèrement différents, mais aussi et surtout des microarchitectures assez différentes. Afin de simplifier les explications, 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. * MEME désigne un accès mémoire, à savoir une instruction ou µop mémoire. * BRANCH désigne une instruction de branchement. Le PowerPC 603 utilisait une double émission "parfaite", avec émission parallèle des branchements. En clair, il pouvait émettre deux instructions + un branchement. Il intégrait une ALU entière, une FPU, l'unité de branchement et l'unité mémoire. Toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. Il avait donc une réelle double émission, ce qui fait qu'il était qualifié de ''CPU 2 + Branch''. Le PowerPC 604 est lui passé à la quadruple émission, avec émission parallèle des branchements. En clair, c'était un CPU de type ''4 + Branch''. 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, mais c'était le seul à être partagé. En clair, le CPU pouvait émettre deux INT + MEM + (soit MUL, soit FLOAT). Vint ensuite la génération suivante avec les PowerPC G3 et G4. Ils implémentaient la double émission + branchement. La différence avec le 603 est qu'ils intégraient deux ALU entières, pas une seule. Ils pouvaient donc exécuter deux opérations entières en même temps, la seule contrainte étant sur les multiplications (il n'y a qu'un seul multiplieur). Les combinaisons possibles étaient INT + INT , INT + MUL, INT + FLOAT, INT + MEM, FLOAT + MEM. La '''double émission flottante''' a été utilisée sur les processeurs Alpha et SPARC. L'idée est d'émettre deux opérations flottantes à la fois. Sauf qu'une implémentation naïve demanderait de dupliquer la FPU, ce qui aurait un cout en circuit rédhibitoire. Pour rappel, les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant. L'idée est d'avoir des ports d'émission séparés pour l'additionneur flottant et le multiplieur flottant. Le processeur peut alors émettre une addition flottante en même temps qu'une multiplication flottante. Les autres circuits de calcul flottant sont répartis sur ces deux ports d'émission flottants. L'avantage est que cela se marie bien avec l'usage d'une fenêtre d'instruction séparée pour les opérations flottantes. La fenêtre d'instruction a alors deux ports séparés, au lieu d'un seul. Rajouter un second port d'émission flottant n'est pas trop un problème, car le cout lié à l'ajout d'un port n'est pas linéaire. Passer de un port à deux a un cout tolérable, bien plus que de passer de 3 ports à 4 ou de 4 à 5. Il est possible de dupliquer l'additionneur et le multiplieur flottant, ce qui permet d'émettre deux additions et multiplications flottantes en même temps. C'est ce qui est fait sur les processeurs AMD d'architecture Zen 1 et 2. Ils ont deux additionneurs flottants par cœur, deux multiplieurs flottants, chacun avec leur propre port d'émission. Les performances en calcul flottant sont assez impressionnantes pour un processeur de l'époque. [[File:ZEN - émission multiple flottante.png|centre|vignette|upright=2|Microarchitecture Zen 1 d'AMD.]] Il existe des processeurs où un même port d'émission alimente à la fois une ALU entière et une FPU. Par exemple, on peut relier un additionneur flottant sur le même port qu'une ALU entière. Il faut noter que cela implique une fenêtre d'instruction centralisée, capable de mettre en attente micro-opérations entières et flottantes. Un exemple est celui des processeurs Core 2 Duo. Ils disposent de 6 ports d'émission, dont 3 ports dédiés à l'unité mémoire. Les 3 ports restants alimentent chacun : une ALU entière, un circuit de calcul flottant et une unité de calcul SIMD (on abordera le SIMD dans quelques chapitres). * Le premier port alimente une ALU entière simple et un multiplieur/diviseur flottant. * Le second alimente une ALU entière, un multiplieur entier et un additionneur flottant. * Le troisième alimente une ALU entière, sans circuit flottant dédié. ==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 simultanné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=== 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. Elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. La ''load/store queue'' reste une structure unique, mais on peut lui rajouter des ports. Et à ce peit jeu, il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire. [[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]] 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. Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre. Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Le Pentium 4 était dans ce cas, mais il était un peu seul dans sa catégorie. Un cas similaire est celui où l'unité mémoire est alimentée par une fenêtre de micro-opération mélangeant instructions mémoire et entières. Les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. Dans ce cas, l'émission multiple est rarement implémentée. Le cas suivant a une file pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique. Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui pouvaient seulement émettre une lecture en même temps qu'une écriture. Ils avaient deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse. [[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]] Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture demande d'utiliser une file d'écriture multiport, ce qui est assez compliqué. Implémenter des lectures en plus est plus simple : il suffit de rajouter une seconde file de µops LOAD et une unité de calcul, alimentées via un second port d'émission. Il est aussi possible de rajouter un second port de lecture au cache, mais ce n'est pas obligatoire. Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse. De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports de lecture et d'écriture car il y a plus de lecture que d'écritures. Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. : Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse. [[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]] Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettent plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail. ==Les optimisations des processeurs superscalaires larges== Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul. ===L'étape de chargement d'un CPU superscalaire large=== Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 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 autoaligné.]] Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements ! [[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]] Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez 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 microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total). Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois 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 microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement. Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations. ===La micro-fusion des processeurs Intel et AMD=== La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''. Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''. Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout. La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Le parallélisme mémoire | prevText=Le parallélisme mémoire | next=Exemples de microarchitectures CPU : le cas du x86 | nextText=Exemples de microarchitectures CPU : le cas du x86 }} </noinclude> oflqxiqgwlcd24ga0u4dai5gt6x22gq 772746 772745 2026-09-21T20:45:56Z Mewtow 31375 /* Les CPU superscalaires à plusieurs unités spécialisées */ 772746 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. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps. 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. 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. Deux branchements consécutifs sont quant à eux plus fréquents, mais exécuter deux branchements en même temps pose un problème : que faire quand les deux sont pris ? Deux accès mémoire consécutifs est assez rare. La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs sont systématiquement supportées, car dupliquer des ALU 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 et 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'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. 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. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant. 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. ===Le chargement de plusieurs instructions consécutives=== Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large. Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence. Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas. Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement non-pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc. [[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]] ===Le décodage parallèle des instructions=== Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits. Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur. Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! ===L'unité d'émission d'un processeur superscalaire=== La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. 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 4 instructions, compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément deux 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. 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. : 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 microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle. 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. ==L'évolution historique des processeurs superscalaires== L'évolution des CPU superscalaires historiques 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. Leur historique distingue six étapes, mais je vais en omettre une et fusionner les deux dernières. Le résultat est une évolution en quatre temps, qui portent les noms de : * double émission entière-flottante ; * exécution parallèle des branchements ; * émission multiple des opérations entières. Les tout premiers processeurs superscalaires avaient un budget en transistors très limité, ce qui fait que les contraintes d'appariement étaient très strictes. Les deux premières étapes ne faisaient que mieux utiliser des unités de calcul déjà présentes, ils ne dupliquaient aucune unité de calcul. La troisième a ajouté des ALUs entières, pour améliorer un peu les performances. Après la quatrième étape, les CPU superscalaires sont devenus très variés, au point de ne pas pouvoir être classés simplement. ===La double émission entière-flottante=== Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est d'émettre une micro-opération entière en même temps qu'une micro-opé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, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité. La plupart des premiers processeurs superscalaires étaient de ce type. Les exemples les plus notables sont les processeurs POWER 1 et ses dérivés comme le ''RISC Single Chip''. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle. [[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]] La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisations visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate]. ===L'émission parallèle des branchements=== Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur. 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|Emission multiple des opérations entières, implémentation courante]] Pour les branchements, deux solutions sont possibles. Avec la première, il y a émission parallèle des branchements, à savoir qu'il est possible d'émettre en branchement en parallèle de deux-trois instructions entières. Et cela impose la présence d'une unité de branchement séparée, avec son port d'émission. Avec la seconde, les branchements sont traités comme des instructions entières. Soit les branchements sont exécutés dans une ALU entière, soit il y a une unité de branchement reliée à un port d'émission pour les opérations entières. [[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Emission série et parallèle des branchements]] En général, les branchements sont exécutés comme des opérations entières. Cela permet d'économiser une unité de calcul et beaucoup de transistors dans l'unité d'émission/chargement/... {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 |- | * ALU entière * Multiplieur | * ALU entière * Barrel shifter * Unité de branchement | FPU |- | * Additions, soustractions, opérations bit à bit, etc. * Multiplication | * Additions, soustractions, opérations bit à bit, etc. * Décalages et rotations * Branchements |Opération flottante |} ===Les CPU superscalaires à plusieurs unités spécialisées=== L'évolution des CPU superscalaires s'est faite en quatre temps, et nous en sommes à la quatrième étape. Elle ne correspond en réalité pas à grand chose de concret, ni à une évolution technique bien précise. Elle correspond en réalité à l'arrivée de processeurs superscalaires très variés, qui n'ont pas grand chose en commun. Le premier CPU superscalaire grand public était le Pentium. Et c'était un CPU double émission qui n'implémentait pas la double émission entière-flottante, mais qui avait deux ALU entières. 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. Ils ont eux aussi privilégié l'ajout d'ALU entières. Les multiplieurs n'ont pas été dupliqués, sauf sur les processeurs superscalaires très larges. La raison est qu'il est rare de devoir exécuter deux multiplications à la fois. Et c'est la même chose pour le ''barrel shifter''. Pour donner quelques exemples, étudions les processeurs superscalaire PowerPC. Il y a eu plusieurs modèles, avec des jeux d'instructions légèrement différents, mais aussi et surtout des microarchitectures assez différentes. Afin de simplifier les explications, 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. Le PowerPC 603 utilisait une double émission "parfaite", avec émission parallèle des branchements. En clair, il pouvait émettre deux instructions + un branchement. Il intégrait une ALU entière, une FPU, l'unité de branchement et l'unité mémoire. Toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. Il avait donc une réelle double émission, ce qui fait qu'il était qualifié de ''CPU 2 + Branch''. Le PowerPC 604 est lui passé à la quadruple émission, avec émission parallèle des branchements. En clair, c'était un CPU de type ''4 + Branch''. 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, mais c'était le seul à être partagé. En clair, le CPU pouvait émettre deux INT + MEM + (soit MUL, soit FLOAT). Vint ensuite la génération suivante avec les PowerPC G3 et G4. Ils implémentaient la double émission + branchement. La différence avec le 603 est qu'ils intégraient deux ALU entières, pas une seule. Ils pouvaient donc exécuter deux opérations entières en même temps, la seule contrainte étant sur les multiplications (il n'y a qu'un seul multiplieur). Les combinaisons possibles étaient INT + INT , INT + MUL, INT + FLOAT, INT + MEM, FLOAT + MEM. La '''double émission flottante''' a été utilisée sur les processeurs Alpha et SPARC. L'idée est d'émettre deux opérations flottantes à la fois. Sauf qu'une implémentation naïve demanderait de dupliquer la FPU, ce qui aurait un cout en circuit rédhibitoire. Pour rappel, les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant. L'idée est d'avoir des ports d'émission séparés pour l'additionneur flottant et le multiplieur flottant. Le processeur peut alors émettre une addition flottante en même temps qu'une multiplication flottante. Les autres circuits de calcul flottant sont répartis sur ces deux ports d'émission flottants. L'avantage est que cela se marie bien avec l'usage d'une fenêtre d'instruction séparée pour les opérations flottantes. La fenêtre d'instruction a alors deux ports séparés, au lieu d'un seul. Rajouter un second port d'émission flottant n'est pas trop un problème, car le cout lié à l'ajout d'un port n'est pas linéaire. Passer de un port à deux a un cout tolérable, bien plus que de passer de 3 ports à 4 ou de 4 à 5. Il est possible de dupliquer l'additionneur et le multiplieur flottant, ce qui permet d'émettre deux additions et multiplications flottantes en même temps. C'est ce qui est fait sur les processeurs AMD d'architecture Zen 1 et 2. Ils ont deux additionneurs flottants par cœur, deux multiplieurs flottants, chacun avec leur propre port d'émission. Les performances en calcul flottant sont assez impressionnantes pour un processeur de l'époque. [[File:ZEN - émission multiple flottante.png|centre|vignette|upright=2|Microarchitecture Zen 1 d'AMD.]] Il existe des processeurs où un même port d'émission alimente à la fois une ALU entière et une FPU. Par exemple, on peut relier un additionneur flottant sur le même port qu'une ALU entière. Il faut noter que cela implique une fenêtre d'instruction centralisée, capable de mettre en attente micro-opérations entières et flottantes. Un exemple est celui des processeurs Core 2 Duo. Ils disposent de 6 ports d'émission, dont 3 ports dédiés à l'unité mémoire. Les 3 ports restants alimentent chacun : une ALU entière, un circuit de calcul flottant et une unité de calcul SIMD (on abordera le SIMD dans quelques chapitres). * Le premier port alimente une ALU entière simple et un multiplieur/diviseur flottant. * Le second alimente une ALU entière, un multiplieur entier et un additionneur flottant. * Le troisième alimente une ALU entière, sans circuit flottant dédié. ==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 simultanné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=== 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. Elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. La ''load/store queue'' reste une structure unique, mais on peut lui rajouter des ports. Et à ce peit jeu, il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire. [[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]] 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. Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre. Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Le Pentium 4 était dans ce cas, mais il était un peu seul dans sa catégorie. Un cas similaire est celui où l'unité mémoire est alimentée par une fenêtre de micro-opération mélangeant instructions mémoire et entières. Les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. Dans ce cas, l'émission multiple est rarement implémentée. Le cas suivant a une file pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique. Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui pouvaient seulement émettre une lecture en même temps qu'une écriture. Ils avaient deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse. [[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]] Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture demande d'utiliser une file d'écriture multiport, ce qui est assez compliqué. Implémenter des lectures en plus est plus simple : il suffit de rajouter une seconde file de µops LOAD et une unité de calcul, alimentées via un second port d'émission. Il est aussi possible de rajouter un second port de lecture au cache, mais ce n'est pas obligatoire. Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse. De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports de lecture et d'écriture car il y a plus de lecture que d'écritures. Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. : Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse. [[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]] Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettent plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail. ==Les optimisations des processeurs superscalaires larges== Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul. ===L'étape de chargement d'un CPU superscalaire large=== Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 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 autoaligné.]] Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements ! [[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]] Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez 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 microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total). Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois 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 microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement. Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations. ===La micro-fusion des processeurs Intel et AMD=== La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''. Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''. Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout. La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Le parallélisme mémoire | prevText=Le parallélisme mémoire | next=Exemples de microarchitectures CPU : le cas du x86 | nextText=Exemples de microarchitectures CPU : le cas du x86 }} </noinclude> 78altdetbf2ygvxtbpczz0nojqz89t8 772747 772746 2026-09-21T20:47:05Z Mewtow 31375 /* Les CPU superscalaires à plusieurs unités spécialisées */ 772747 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. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps. 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. 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. Deux branchements consécutifs sont quant à eux plus fréquents, mais exécuter deux branchements en même temps pose un problème : que faire quand les deux sont pris ? Deux accès mémoire consécutifs est assez rare. La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs sont systématiquement supportées, car dupliquer des ALU 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 et 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'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. 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. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant. 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. ===Le chargement de plusieurs instructions consécutives=== Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large. Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence. Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas. Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement non-pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc. [[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]] ===Le décodage parallèle des instructions=== Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits. Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur. Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! ===L'unité d'émission d'un processeur superscalaire=== La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. 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 4 instructions, compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément deux 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. 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. : 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 microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle. 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. ==L'évolution historique des processeurs superscalaires== L'évolution des CPU superscalaires historiques 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. Leur historique distingue six étapes, mais je vais en omettre une et fusionner les deux dernières. Le résultat est une évolution en quatre temps, qui portent les noms de : * double émission entière-flottante ; * exécution parallèle des branchements ; * émission multiple des opérations entières. Les tout premiers processeurs superscalaires avaient un budget en transistors très limité, ce qui fait que les contraintes d'appariement étaient très strictes. Les deux premières étapes ne faisaient que mieux utiliser des unités de calcul déjà présentes, ils ne dupliquaient aucune unité de calcul. La troisième a ajouté des ALUs entières, pour améliorer un peu les performances. Après la quatrième étape, les CPU superscalaires sont devenus très variés, au point de ne pas pouvoir être classés simplement. ===La double émission entière-flottante=== Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est d'émettre une micro-opération entière en même temps qu'une micro-opé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, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité. La plupart des premiers processeurs superscalaires étaient de ce type. Les exemples les plus notables sont les processeurs POWER 1 et ses dérivés comme le ''RISC Single Chip''. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle. [[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]] La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisations visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate]. ===L'émission parallèle des branchements=== Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur. 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|Emission multiple des opérations entières, implémentation courante]] Pour les branchements, deux solutions sont possibles. Avec la première, il y a émission parallèle des branchements, à savoir qu'il est possible d'émettre en branchement en parallèle de deux-trois instructions entières. Et cela impose la présence d'une unité de branchement séparée, avec son port d'émission. Avec la seconde, les branchements sont traités comme des instructions entières. Soit les branchements sont exécutés dans une ALU entière, soit il y a une unité de branchement reliée à un port d'émission pour les opérations entières. [[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Emission série et parallèle des branchements]] En général, les branchements sont exécutés comme des opérations entières. Cela permet d'économiser une unité de calcul et beaucoup de transistors dans l'unité d'émission/chargement/... {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 |- | * ALU entière * Multiplieur | * ALU entière * Barrel shifter * Unité de branchement | FPU |- | * Additions, soustractions, opérations bit à bit, etc. * Multiplication | * Additions, soustractions, opérations bit à bit, etc. * Décalages et rotations * Branchements |Opération flottante |} ===Les CPU superscalaires à plusieurs unités spécialisées=== L'évolution des CPU superscalaires s'est faite en quatre temps, et nous en sommes à la quatrième étape. Elle ne correspond en réalité pas à grand chose de concret, ni à une évolution technique bien précise. Elle correspond en réalité à l'arrivée de processeurs superscalaires très variés, qui n'ont pas grand chose en commun. Le premier CPU superscalaire grand public était le Pentium. Et c'était un CPU double émission qui n'implémentait pas la double émission entière-flottante, mais qui avait deux ALU entières. 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. Ils ont eux aussi privilégié l'ajout d'ALU entières. Les multiplieurs n'ont pas été dupliqués, sauf sur les processeurs superscalaires très larges. La raison est qu'il est rare de devoir exécuter deux multiplications à la fois. Et c'est la même chose pour le ''barrel shifter''. Pour donner quelques exemples, étudions les processeurs superscalaire PowerPC. Il y a eu plusieurs modèles, avec des jeux d'instructions légèrement différents, mais aussi et surtout des microarchitectures assez différentes. Afin de simplifier les explications, 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. Le PowerPC 603 utilisait une double émission "parfaite", avec émission parallèle des branchements. En clair, il pouvait émettre deux instructions + un branchement. Il intégrait une ALU entière, une FPU, l'unité de branchement et l'unité mémoire. Toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. Il avait donc une réelle double émission, ce qui fait qu'il était qualifié de ''CPU 2 + Branch''. Le PowerPC 604 est lui passé à la quadruple émission, avec émission parallèle des branchements. En clair, c'était un CPU de type ''4 + Branch''. 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, mais c'était le seul à être partagé. En clair, le CPU pouvait émettre deux INT + MEM + (soit MUL, soit FLOAT). Vint ensuite la génération suivante avec les PowerPC G3 et G4. Ils implémentaient la double émission + branchement. La différence avec le 603 est qu'ils intégraient deux ALU entières, pas une seule. Ils pouvaient donc exécuter deux opérations entières en même temps, la seule contrainte étant sur les multiplications (il n'y a qu'un seul multiplieur). Les combinaisons possibles étaient INT + INT , INT + MUL, INT + FLOAT, INT + MEM, FLOAT + MEM. La '''double émission flottante''' a été utilisée sur les processeurs Alpha et SPARC. L'idée est d'émettre deux opérations flottantes à la fois. Sauf qu'une implémentation naïve demanderait de dupliquer la FPU, ce qui aurait un cout en circuit rédhibitoire. Pour rappel, les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant. L'idée est d'avoir des ports d'émission séparés pour l'additionneur flottant et le multiplieur flottant. Le processeur peut alors émettre une addition flottante en même temps qu'une multiplication flottante. Les autres circuits de calcul flottant sont répartis sur ces deux ports d'émission flottants. L'avantage est que cela se marie bien avec l'usage d'une fenêtre d'instruction séparée pour les opérations flottantes. La fenêtre d'instruction a alors deux ports séparés, au lieu d'un seul. Rajouter un second port d'émission flottant n'est pas trop un problème, car le cout lié à l'ajout d'un port n'est pas linéaire. Passer de un port à deux a un cout tolérable, bien plus que de passer de 3 ports à 4 ou de 4 à 5. ==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 simultanné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=== 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. Elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. La ''load/store queue'' reste une structure unique, mais on peut lui rajouter des ports. Et à ce peit jeu, il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire. [[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]] 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. Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre. Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Le Pentium 4 était dans ce cas, mais il était un peu seul dans sa catégorie. Un cas similaire est celui où l'unité mémoire est alimentée par une fenêtre de micro-opération mélangeant instructions mémoire et entières. Les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. Dans ce cas, l'émission multiple est rarement implémentée. Le cas suivant a une file pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique. Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui pouvaient seulement émettre une lecture en même temps qu'une écriture. Ils avaient deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse. [[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]] Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture demande d'utiliser une file d'écriture multiport, ce qui est assez compliqué. Implémenter des lectures en plus est plus simple : il suffit de rajouter une seconde file de µops LOAD et une unité de calcul, alimentées via un second port d'émission. Il est aussi possible de rajouter un second port de lecture au cache, mais ce n'est pas obligatoire. Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse. De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports de lecture et d'écriture car il y a plus de lecture que d'écritures. Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. : Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse. [[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]] Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettent plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail. ==Les optimisations des processeurs superscalaires larges== Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul. ===L'étape de chargement d'un CPU superscalaire large=== Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 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 autoaligné.]] Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements ! [[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]] Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez 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 microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total). Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois 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 microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement. Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations. ===La micro-fusion des processeurs Intel et AMD=== La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''. Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''. Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout. La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Le parallélisme mémoire | prevText=Le parallélisme mémoire | next=Exemples de microarchitectures CPU : le cas du x86 | nextText=Exemples de microarchitectures CPU : le cas du x86 }} </noinclude> qyzfw8p368zwh5ily3yc3s6fnhuk537 772748 772747 2026-09-21T20:51:10Z Mewtow 31375 /* Les unités de calcul d'un processeur superscalaire */ 772748 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. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps. 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. 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. Deux branchements consécutifs sont quant à eux plus fréquents, mais exécuter deux branchements en même temps pose un problème : que faire quand les deux sont pris ? Deux accès mémoire consécutifs est assez rare. La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs sont systématiquement supportées, car dupliquer des ALU 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 et 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'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. 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. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant. Seuls les CPU superscalaires très larges se permettent de dupliquer le multiplieur ou la FPU. 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. ===Le chargement de plusieurs instructions consécutives=== Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large. Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence. Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas. Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement non-pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc. [[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]] ===Le décodage parallèle des instructions=== Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits. Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur. Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! ===L'unité d'émission d'un processeur superscalaire=== La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. 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 4 instructions, compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément deux 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. 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. : 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 microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle. 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. ==L'évolution historique des processeurs superscalaires== L'évolution des CPU superscalaires historiques 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. Leur historique distingue six étapes, mais je vais en omettre une et fusionner les deux dernières. Le résultat est une évolution en quatre temps, qui portent les noms de : * double émission entière-flottante ; * exécution parallèle des branchements ; * émission multiple des opérations entières. Les tout premiers processeurs superscalaires avaient un budget en transistors très limité, ce qui fait que les contraintes d'appariement étaient très strictes. Les deux premières étapes ne faisaient que mieux utiliser des unités de calcul déjà présentes, ils ne dupliquaient aucune unité de calcul. La troisième a ajouté des ALUs entières, pour améliorer un peu les performances. Après la quatrième étape, les CPU superscalaires sont devenus très variés, au point de ne pas pouvoir être classés simplement. ===La double émission entière-flottante=== Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est d'émettre une micro-opération entière en même temps qu'une micro-opé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, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité. La plupart des premiers processeurs superscalaires étaient de ce type. Les exemples les plus notables sont les processeurs POWER 1 et ses dérivés comme le ''RISC Single Chip''. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle. [[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]] La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisations visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate]. ===L'émission parallèle des branchements=== Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur. 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|Emission multiple des opérations entières, implémentation courante]] Pour les branchements, deux solutions sont possibles. Avec la première, il y a émission parallèle des branchements, à savoir qu'il est possible d'émettre en branchement en parallèle de deux-trois instructions entières. Et cela impose la présence d'une unité de branchement séparée, avec son port d'émission. Avec la seconde, les branchements sont traités comme des instructions entières. Soit les branchements sont exécutés dans une ALU entière, soit il y a une unité de branchement reliée à un port d'émission pour les opérations entières. [[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Emission série et parallèle des branchements]] En général, les branchements sont exécutés comme des opérations entières. Cela permet d'économiser une unité de calcul et beaucoup de transistors dans l'unité d'émission/chargement/... {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 |- | * ALU entière * Multiplieur | * ALU entière * Barrel shifter * Unité de branchement | FPU |- | * Additions, soustractions, opérations bit à bit, etc. * Multiplication | * Additions, soustractions, opérations bit à bit, etc. * Décalages et rotations * Branchements |Opération flottante |} ===Les CPU superscalaires à plusieurs unités spécialisées=== L'évolution des CPU superscalaires s'est faite en quatre temps, et nous en sommes à la quatrième étape. Elle ne correspond en réalité pas à grand chose de concret, ni à une évolution technique bien précise. Elle correspond en réalité à l'arrivée de processeurs superscalaires très variés, qui n'ont pas grand chose en commun. Le premier CPU superscalaire grand public était le Pentium. Et c'était un CPU double émission qui n'implémentait pas la double émission entière-flottante, mais qui avait deux ALU entières. 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. Ils ont eux aussi privilégié l'ajout d'ALU entières. Les multiplieurs n'ont pas été dupliqués, sauf sur les processeurs superscalaires très larges. La raison est qu'il est rare de devoir exécuter deux multiplications à la fois. Et c'est la même chose pour le ''barrel shifter''. Pour donner quelques exemples, étudions les processeurs superscalaire PowerPC. Il y a eu plusieurs modèles, avec des jeux d'instructions légèrement différents, mais aussi et surtout des microarchitectures assez différentes. Afin de simplifier les explications, 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. Le PowerPC 603 utilisait une double émission "parfaite", avec émission parallèle des branchements. En clair, il pouvait émettre deux instructions + un branchement. Il intégrait une ALU entière, une FPU, l'unité de branchement et l'unité mémoire. Toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. Il avait donc une réelle double émission, ce qui fait qu'il était qualifié de ''CPU 2 + Branch''. Le PowerPC 604 est lui passé à la quadruple émission, avec émission parallèle des branchements. En clair, c'était un CPU de type ''4 + Branch''. 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, mais c'était le seul à être partagé. En clair, le CPU pouvait émettre deux INT + MEM + (soit MUL, soit FLOAT). Vint ensuite la génération suivante avec les PowerPC G3 et G4. Ils implémentaient la double émission + branchement. La différence avec le 603 est qu'ils intégraient deux ALU entières, pas une seule. Ils pouvaient donc exécuter deux opérations entières en même temps, la seule contrainte étant sur les multiplications (il n'y a qu'un seul multiplieur). Les combinaisons possibles étaient INT + INT , INT + MUL, INT + FLOAT, INT + MEM, FLOAT + MEM. La '''double émission flottante''' a été utilisée sur les processeurs Alpha et SPARC. L'idée est d'émettre deux opérations flottantes à la fois. Sauf qu'une implémentation naïve demanderait de dupliquer la FPU, ce qui aurait un cout en circuit rédhibitoire. Pour rappel, les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant. L'idée est d'avoir des ports d'émission séparés pour l'additionneur flottant et le multiplieur flottant. Le processeur peut alors émettre une addition flottante en même temps qu'une multiplication flottante. Les autres circuits de calcul flottant sont répartis sur ces deux ports d'émission flottants. L'avantage est que cela se marie bien avec l'usage d'une fenêtre d'instruction séparée pour les opérations flottantes. La fenêtre d'instruction a alors deux ports séparés, au lieu d'un seul. Rajouter un second port d'émission flottant n'est pas trop un problème, car le cout lié à l'ajout d'un port n'est pas linéaire. Passer de un port à deux a un cout tolérable, bien plus que de passer de 3 ports à 4 ou de 4 à 5. ==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 simultanné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=== 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. Elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. La ''load/store queue'' reste une structure unique, mais on peut lui rajouter des ports. Et à ce peit jeu, il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire. [[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]] 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. Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre. Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Le Pentium 4 était dans ce cas, mais il était un peu seul dans sa catégorie. Un cas similaire est celui où l'unité mémoire est alimentée par une fenêtre de micro-opération mélangeant instructions mémoire et entières. Les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. Dans ce cas, l'émission multiple est rarement implémentée. Le cas suivant a une file pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique. Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui pouvaient seulement émettre une lecture en même temps qu'une écriture. Ils avaient deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse. [[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]] Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture demande d'utiliser une file d'écriture multiport, ce qui est assez compliqué. Implémenter des lectures en plus est plus simple : il suffit de rajouter une seconde file de µops LOAD et une unité de calcul, alimentées via un second port d'émission. Il est aussi possible de rajouter un second port de lecture au cache, mais ce n'est pas obligatoire. Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse. De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports de lecture et d'écriture car il y a plus de lecture que d'écritures. Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. : Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse. [[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]] Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettent plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail. ==Les optimisations des processeurs superscalaires larges== Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul. ===L'étape de chargement d'un CPU superscalaire large=== Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 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 autoaligné.]] Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements ! [[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]] Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez 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 microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total). Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois 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 microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement. Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations. ===La micro-fusion des processeurs Intel et AMD=== La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''. Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''. Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout. La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Le parallélisme mémoire | prevText=Le parallélisme mémoire | next=Exemples de microarchitectures CPU : le cas du x86 | nextText=Exemples de microarchitectures CPU : le cas du x86 }} </noinclude> fg0zngdu0yzzskn5jr8d5f1mnjodhrn 772749 772748 2026-09-21T20:52:55Z Mewtow 31375 /* Les unités de calcul d'un processeur superscalaire */ 772749 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. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps. 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. 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. Deux branchements consécutifs sont quant à eux plus fréquents, mais exécuter deux branchements en même temps pose un problème : que faire quand les deux sont pris ? Deux accès mémoire consécutifs est assez rare. La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs sont systématiquement supportées, car dupliquer des ALU 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 et 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'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. 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. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant. 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. ===Le chargement de plusieurs instructions consécutives=== Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large. Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence. Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas. Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement non-pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc. [[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]] ===Le décodage parallèle des instructions=== Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits. Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur. Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! ===L'unité d'émission d'un processeur superscalaire=== La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. 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 4 instructions, compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément deux 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. 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. : 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 microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle. 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. ==L'évolution historique des processeurs superscalaires== L'évolution des CPU superscalaires historiques 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. Leur historique distingue six étapes, mais je vais en omettre une et fusionner les deux dernières. Le résultat est une évolution en quatre temps, qui portent les noms de : * double émission entière-flottante ; * exécution parallèle des branchements ; * émission multiple des opérations entières. Les tout premiers processeurs superscalaires avaient un budget en transistors très limité, ce qui fait que les contraintes d'appariement étaient très strictes. Les deux premières étapes ne faisaient que mieux utiliser des unités de calcul déjà présentes, ils ne dupliquaient aucune unité de calcul. La troisième a ajouté des ALUs entières, pour améliorer un peu les performances. Après la quatrième étape, les CPU superscalaires sont devenus très variés, au point de ne pas pouvoir être classés simplement. ===La double émission entière-flottante=== Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est d'émettre une micro-opération entière en même temps qu'une micro-opé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, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité. La plupart des premiers processeurs superscalaires étaient de ce type. Les exemples les plus notables sont les processeurs POWER 1 et ses dérivés comme le ''RISC Single Chip''. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle. [[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]] La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisations visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate]. ===L'émission parallèle des branchements=== Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur. 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|Emission multiple des opérations entières, implémentation courante]] Pour les branchements, deux solutions sont possibles. Avec la première, il y a émission parallèle des branchements, à savoir qu'il est possible d'émettre en branchement en parallèle de deux-trois instructions entières. Et cela impose la présence d'une unité de branchement séparée, avec son port d'émission. Avec la seconde, les branchements sont traités comme des instructions entières. Soit les branchements sont exécutés dans une ALU entière, soit il y a une unité de branchement reliée à un port d'émission pour les opérations entières. [[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Emission série et parallèle des branchements]] En général, les branchements sont exécutés comme des opérations entières. Cela permet d'économiser une unité de calcul et beaucoup de transistors dans l'unité d'émission/chargement/... {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 |- | * ALU entière * Multiplieur | * ALU entière * Barrel shifter * Unité de branchement | FPU |- | * Additions, soustractions, opérations bit à bit, etc. * Multiplication | * Additions, soustractions, opérations bit à bit, etc. * Décalages et rotations * Branchements |Opération flottante |} ===Les CPU superscalaires à plusieurs unités spécialisées=== L'évolution des CPU superscalaires s'est faite en quatre temps, et nous en sommes à la quatrième étape. Elle ne correspond en réalité pas à grand chose de concret, ni à une évolution technique bien précise. Elle correspond en réalité à l'arrivée de processeurs superscalaires très variés, qui n'ont pas grand chose en commun. Le premier CPU superscalaire grand public était le Pentium. Et c'était un CPU double émission qui n'implémentait pas la double émission entière-flottante, mais qui avait deux ALU entières. 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. Ils ont eux aussi privilégié l'ajout d'ALU entières. Les multiplieurs n'ont pas été dupliqués, sauf sur les processeurs superscalaires très larges. La raison est qu'il est rare de devoir exécuter deux multiplications à la fois. Et c'est la même chose pour le ''barrel shifter''. Pour donner quelques exemples, étudions les processeurs superscalaire PowerPC. Il y a eu plusieurs modèles, avec des jeux d'instructions légèrement différents, mais aussi et surtout des microarchitectures assez différentes. Afin de simplifier les explications, 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. Le PowerPC 603 utilisait une double émission "parfaite", avec émission parallèle des branchements. En clair, il pouvait émettre deux instructions + un branchement. Il intégrait une ALU entière, une FPU, l'unité de branchement et l'unité mémoire. Toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. Il avait donc une réelle double émission, ce qui fait qu'il était qualifié de ''CPU 2 + Branch''. Le PowerPC 604 est lui passé à la quadruple émission, avec émission parallèle des branchements. En clair, c'était un CPU de type ''4 + Branch''. 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, mais c'était le seul à être partagé. En clair, le CPU pouvait émettre deux INT + MEM + (soit MUL, soit FLOAT). Vint ensuite la génération suivante avec les PowerPC G3 et G4. Ils implémentaient la double émission + branchement. La différence avec le 603 est qu'ils intégraient deux ALU entières, pas une seule. Ils pouvaient donc exécuter deux opérations entières en même temps, la seule contrainte étant sur les multiplications (il n'y a qu'un seul multiplieur). Les combinaisons possibles étaient INT + INT , INT + MUL, INT + FLOAT, INT + MEM, FLOAT + MEM. La '''double émission flottante''' a été utilisée sur les processeurs Alpha et SPARC. L'idée est d'émettre deux opérations flottantes à la fois. Sauf qu'une implémentation naïve demanderait de dupliquer la FPU, ce qui aurait un cout en circuit rédhibitoire. Pour rappel, les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant. L'idée est d'avoir des ports d'émission séparés pour l'additionneur flottant et le multiplieur flottant. Le processeur peut alors émettre une addition flottante en même temps qu'une multiplication flottante. Les autres circuits de calcul flottant sont répartis sur ces deux ports d'émission flottants. L'avantage est que cela se marie bien avec l'usage d'une fenêtre d'instruction séparée pour les opérations flottantes. La fenêtre d'instruction a alors deux ports séparés, au lieu d'un seul. Rajouter un second port d'émission flottant n'est pas trop un problème, car le cout lié à l'ajout d'un port n'est pas linéaire. Passer de un port à deux a un cout tolérable, bien plus que de passer de 3 ports à 4 ou de 4 à 5. ==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 simultanné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=== 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. Elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. La ''load/store queue'' reste une structure unique, mais on peut lui rajouter des ports. Et à ce peit jeu, il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire. [[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]] 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. Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre. Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Le Pentium 4 était dans ce cas, mais il était un peu seul dans sa catégorie. Un cas similaire est celui où l'unité mémoire est alimentée par une fenêtre de micro-opération mélangeant instructions mémoire et entières. Les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. Dans ce cas, l'émission multiple est rarement implémentée. Le cas suivant a une file pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique. Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui pouvaient seulement émettre une lecture en même temps qu'une écriture. Ils avaient deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse. [[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]] Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture demande d'utiliser une file d'écriture multiport, ce qui est assez compliqué. Implémenter des lectures en plus est plus simple : il suffit de rajouter une seconde file de µops LOAD et une unité de calcul, alimentées via un second port d'émission. Il est aussi possible de rajouter un second port de lecture au cache, mais ce n'est pas obligatoire. Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse. De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports de lecture et d'écriture car il y a plus de lecture que d'écritures. Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. : Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse. [[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]] Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettent plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail. ==Les optimisations des processeurs superscalaires larges== Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul. ===L'étape de chargement d'un CPU superscalaire large=== Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 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 autoaligné.]] Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements ! [[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]] Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez 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 microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total). Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois 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 microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement. Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations. ===La micro-fusion des processeurs Intel et AMD=== La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''. Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''. Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout. La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Le parallélisme mémoire | prevText=Le parallélisme mémoire | next=Exemples de microarchitectures CPU : le cas du x86 | nextText=Exemples de microarchitectures CPU : le cas du x86 }} </noinclude> qyzfw8p368zwh5ily3yc3s6fnhuk537 772755 772749 2026-09-21T21:16:28Z Mewtow 31375 /* Le décodage parallèle des instructions */ 772755 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. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps. 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. 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. Deux branchements consécutifs sont quant à eux plus fréquents, mais exécuter deux branchements en même temps pose un problème : que faire quand les deux sont pris ? Deux accès mémoire consécutifs est assez rare. La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs sont systématiquement supportées, car dupliquer des ALU 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 et 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'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. 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. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant. 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. ===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 4 instructions, compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément deux 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. 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. : 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 microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle. 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. ==L'évolution historique des processeurs superscalaires== L'évolution des CPU superscalaires historiques 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. Leur historique distingue six étapes, mais je vais en omettre une et fusionner les deux dernières. Le résultat est une évolution en quatre temps, qui portent les noms de : * double émission entière-flottante ; * exécution parallèle des branchements ; * émission multiple des opérations entières. Les tout premiers processeurs superscalaires avaient un budget en transistors très limité, ce qui fait que les contraintes d'appariement étaient très strictes. Les deux premières étapes ne faisaient que mieux utiliser des unités de calcul déjà présentes, ils ne dupliquaient aucune unité de calcul. La troisième a ajouté des ALUs entières, pour améliorer un peu les performances. Après la quatrième étape, les CPU superscalaires sont devenus très variés, au point de ne pas pouvoir être classés simplement. ===La double émission entière-flottante=== Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est d'émettre une micro-opération entière en même temps qu'une micro-opé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, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité. La plupart des premiers processeurs superscalaires étaient de ce type. Les exemples les plus notables sont les processeurs POWER 1 et ses dérivés comme le ''RISC Single Chip''. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle. [[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]] La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisations visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate]. ===L'émission parallèle des branchements=== Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur. 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|Emission multiple des opérations entières, implémentation courante]] Pour les branchements, deux solutions sont possibles. Avec la première, il y a émission parallèle des branchements, à savoir qu'il est possible d'émettre en branchement en parallèle de deux-trois instructions entières. Et cela impose la présence d'une unité de branchement séparée, avec son port d'émission. Avec la seconde, les branchements sont traités comme des instructions entières. Soit les branchements sont exécutés dans une ALU entière, soit il y a une unité de branchement reliée à un port d'émission pour les opérations entières. [[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Emission série et parallèle des branchements]] En général, les branchements sont exécutés comme des opérations entières. Cela permet d'économiser une unité de calcul et beaucoup de transistors dans l'unité d'émission/chargement/... {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 |- | * ALU entière * Multiplieur | * ALU entière * Barrel shifter * Unité de branchement | FPU |- | * Additions, soustractions, opérations bit à bit, etc. * Multiplication | * Additions, soustractions, opérations bit à bit, etc. * Décalages et rotations * Branchements |Opération flottante |} ===Les CPU superscalaires à plusieurs unités spécialisées=== L'évolution des CPU superscalaires s'est faite en quatre temps, et nous en sommes à la quatrième étape. Elle ne correspond en réalité pas à grand chose de concret, ni à une évolution technique bien précise. Elle correspond en réalité à l'arrivée de processeurs superscalaires très variés, qui n'ont pas grand chose en commun. Le premier CPU superscalaire grand public était le Pentium. Et c'était un CPU double émission qui n'implémentait pas la double émission entière-flottante, mais qui avait deux ALU entières. 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. Ils ont eux aussi privilégié l'ajout d'ALU entières. Les multiplieurs n'ont pas été dupliqués, sauf sur les processeurs superscalaires très larges. La raison est qu'il est rare de devoir exécuter deux multiplications à la fois. Et c'est la même chose pour le ''barrel shifter''. Pour donner quelques exemples, étudions les processeurs superscalaire PowerPC. Il y a eu plusieurs modèles, avec des jeux d'instructions légèrement différents, mais aussi et surtout des microarchitectures assez différentes. Afin de simplifier les explications, 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. Le PowerPC 603 utilisait une double émission "parfaite", avec émission parallèle des branchements. En clair, il pouvait émettre deux instructions + un branchement. Il intégrait une ALU entière, une FPU, l'unité de branchement et l'unité mémoire. Toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. Il avait donc une réelle double émission, ce qui fait qu'il était qualifié de ''CPU 2 + Branch''. Le PowerPC 604 est lui passé à la quadruple émission, avec émission parallèle des branchements. En clair, c'était un CPU de type ''4 + Branch''. 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, mais c'était le seul à être partagé. En clair, le CPU pouvait émettre deux INT + MEM + (soit MUL, soit FLOAT). Vint ensuite la génération suivante avec les PowerPC G3 et G4. Ils implémentaient la double émission + branchement. La différence avec le 603 est qu'ils intégraient deux ALU entières, pas une seule. Ils pouvaient donc exécuter deux opérations entières en même temps, la seule contrainte étant sur les multiplications (il n'y a qu'un seul multiplieur). Les combinaisons possibles étaient INT + INT , INT + MUL, INT + FLOAT, INT + MEM, FLOAT + MEM. La '''double émission flottante''' a été utilisée sur les processeurs Alpha et SPARC. L'idée est d'émettre deux opérations flottantes à la fois. Sauf qu'une implémentation naïve demanderait de dupliquer la FPU, ce qui aurait un cout en circuit rédhibitoire. Pour rappel, les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant. L'idée est d'avoir des ports d'émission séparés pour l'additionneur flottant et le multiplieur flottant. Le processeur peut alors émettre une addition flottante en même temps qu'une multiplication flottante. Les autres circuits de calcul flottant sont répartis sur ces deux ports d'émission flottants. L'avantage est que cela se marie bien avec l'usage d'une fenêtre d'instruction séparée pour les opérations flottantes. La fenêtre d'instruction a alors deux ports séparés, au lieu d'un seul. Rajouter un second port d'émission flottant n'est pas trop un problème, car le cout lié à l'ajout d'un port n'est pas linéaire. Passer de un port à deux a un cout tolérable, bien plus que de passer de 3 ports à 4 ou de 4 à 5. ==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 simultanné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=== 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. Elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. La ''load/store queue'' reste une structure unique, mais on peut lui rajouter des ports. Et à ce peit jeu, il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire. [[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]] 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. Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre. Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Le Pentium 4 était dans ce cas, mais il était un peu seul dans sa catégorie. Un cas similaire est celui où l'unité mémoire est alimentée par une fenêtre de micro-opération mélangeant instructions mémoire et entières. Les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. Dans ce cas, l'émission multiple est rarement implémentée. Le cas suivant a une file pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique. Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui pouvaient seulement émettre une lecture en même temps qu'une écriture. Ils avaient deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse. [[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]] Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture demande d'utiliser une file d'écriture multiport, ce qui est assez compliqué. Implémenter des lectures en plus est plus simple : il suffit de rajouter une seconde file de µops LOAD et une unité de calcul, alimentées via un second port d'émission. Il est aussi possible de rajouter un second port de lecture au cache, mais ce n'est pas obligatoire. Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse. De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports de lecture et d'écriture car il y a plus de lecture que d'écritures. Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. : Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse. [[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]] Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettent plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail. ==Les optimisations des processeurs superscalaires larges== Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul. ===L'étape de chargement d'un CPU superscalaire large=== Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 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 autoaligné.]] Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements ! [[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]] Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez 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 microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total). Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois 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 microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement. Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations. ===La micro-fusion des processeurs Intel et AMD=== La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''. Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''. Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout. La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Le parallélisme mémoire | prevText=Le parallélisme mémoire | next=Exemples de microarchitectures CPU : le cas du x86 | nextText=Exemples de microarchitectures CPU : le cas du x86 }} </noinclude> 54lfk6jn6hnmcaur3bijbjrb53e8gxk 772758 772755 2026-09-21T21:38:42Z Mewtow 31375 /* L'évolution historique des processeurs superscalaires */ 772758 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. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps. 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. 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. Deux branchements consécutifs sont quant à eux plus fréquents, mais exécuter deux branchements en même temps pose un problème : que faire quand les deux sont pris ? Deux accès mémoire consécutifs est assez rare. La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs sont systématiquement supportées, car dupliquer des ALU 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 et 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'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. 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. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant. 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. ===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 4 instructions, compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément deux 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. 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. : 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 microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle. 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. ==L'évolution historique des processeurs superscalaires== L'évolution des CPU superscalaires historiques 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. 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. 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. Disons qu'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. Par exemple, 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. ===La double émission entière-flottante=== Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est d'émettre une micro-opération entière en même temps qu'une micro-opé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, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité. La plupart des premiers processeurs superscalaires étaient de ce type. Les exemples les plus notables sont les processeurs POWER 1 et ses dérivés comme le ''RISC Single Chip''. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle. [[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]] La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisations visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate]. ===L'émission parallèle des branchements=== Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur. 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|Emission multiple des opérations entières, implémentation courante]] Pour les branchements, deux solutions sont possibles. Avec la première, il y a émission parallèle des branchements, à savoir qu'il est possible d'émettre en branchement en parallèle de deux-trois instructions entières. Et cela impose la présence d'une unité de branchement séparée, avec son port d'émission. Avec la seconde, les branchements sont traités comme des instructions entières. Soit les branchements sont exécutés dans une ALU entière, soit il y a une unité de branchement reliée à un port d'émission pour les opérations entières. [[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Emission série et parallèle des branchements]] En général, les branchements sont exécutés comme des opérations entières. Cela permet d'économiser une unité de calcul et beaucoup de transistors dans l'unité d'émission/chargement/... {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 |- | * ALU entière * Multiplieur | * ALU entière * Barrel shifter * Unité de branchement | FPU |- | * Additions, soustractions, opérations bit à bit, etc. * Multiplication | * Additions, soustractions, opérations bit à bit, etc. * Décalages et rotations * Branchements |Opération flottante |} ===Les CPU superscalaires à plusieurs unités spécialisées=== Le premier CPU superscalaire grand public était le Pentium. Et c'était un CPU double émission qui n'implémentait pas la double émission entière-flottante, mais qui avait deux ALU entières. 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. Ils ont eux aussi privilégié l'ajout d'ALU entières. Les multiplieurs n'ont pas été dupliqués, sauf sur les processeurs superscalaires très larges. La raison est qu'il est rare de devoir exécuter deux multiplications à la fois. Et c'est la même chose pour le ''barrel shifter''. Pour donner quelques exemples, étudions les processeurs superscalaire PowerPC. Il y a eu plusieurs modèles, avec des jeux d'instructions légèrement différents, mais aussi et surtout des microarchitectures assez différentes. Afin de simplifier les explications, 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. Le PowerPC 603 utilisait une double émission "parfaite", avec émission parallèle des branchements. En clair, il pouvait émettre deux instructions + un branchement. Il intégrait une ALU entière, une FPU, l'unité de branchement et l'unité mémoire. Toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. Il avait donc une réelle double émission, ce qui fait qu'il était qualifié de ''CPU 2 + Branch''. Le PowerPC 604 est lui passé à la quadruple émission, avec émission parallèle des branchements. En clair, c'était un CPU de type ''4 + Branch''. 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, mais c'était le seul à être partagé. En clair, le CPU pouvait émettre deux INT + MEM + (soit MUL, soit FLOAT). Vint ensuite la génération suivante avec les PowerPC G3 et G4. Ils implémentaient la double émission + branchement. La différence avec le 603 est qu'ils intégraient deux ALU entières, pas une seule. Ils pouvaient donc exécuter deux opérations entières en même temps, la seule contrainte étant sur les multiplications (il n'y a qu'un seul multiplieur). Les combinaisons possibles étaient INT + INT , INT + MUL, INT + FLOAT, INT + MEM, FLOAT + MEM. La '''double émission flottante''' a été utilisée sur les processeurs Alpha et SPARC. L'idée est d'émettre deux opérations flottantes à la fois. Sauf qu'une implémentation naïve demanderait de dupliquer la FPU, ce qui aurait un cout en circuit rédhibitoire. Pour rappel, les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant. L'idée est d'avoir des ports d'émission séparés pour l'additionneur flottant et le multiplieur flottant. Le processeur peut alors émettre une addition flottante en même temps qu'une multiplication flottante. Les autres circuits de calcul flottant sont répartis sur ces deux ports d'émission flottants. L'avantage est que cela se marie bien avec l'usage d'une fenêtre d'instruction séparée pour les opérations flottantes. La fenêtre d'instruction a alors deux ports séparés, au lieu d'un seul. Rajouter un second port d'émission flottant n'est pas trop un problème, car le cout lié à l'ajout d'un port n'est pas linéaire. Passer de un port à deux a un cout tolérable, bien plus que de passer de 3 ports à 4 ou de 4 à 5. ==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 simultanné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=== 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. Elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. La ''load/store queue'' reste une structure unique, mais on peut lui rajouter des ports. Et à ce peit jeu, il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire. [[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]] 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. Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre. Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Le Pentium 4 était dans ce cas, mais il était un peu seul dans sa catégorie. Un cas similaire est celui où l'unité mémoire est alimentée par une fenêtre de micro-opération mélangeant instructions mémoire et entières. Les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. Dans ce cas, l'émission multiple est rarement implémentée. Le cas suivant a une file pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique. Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui pouvaient seulement émettre une lecture en même temps qu'une écriture. Ils avaient deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse. [[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]] Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture demande d'utiliser une file d'écriture multiport, ce qui est assez compliqué. Implémenter des lectures en plus est plus simple : il suffit de rajouter une seconde file de µops LOAD et une unité de calcul, alimentées via un second port d'émission. Il est aussi possible de rajouter un second port de lecture au cache, mais ce n'est pas obligatoire. Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse. De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports de lecture et d'écriture car il y a plus de lecture que d'écritures. Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. : Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse. [[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]] Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettent plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail. ==Les optimisations des processeurs superscalaires larges== Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul. ===L'étape de chargement d'un CPU superscalaire large=== Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 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 autoaligné.]] Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements ! [[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]] Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez 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 microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total). Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois 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 microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement. Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations. ===La micro-fusion des processeurs Intel et AMD=== La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''. Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''. Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout. La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Le parallélisme mémoire | prevText=Le parallélisme mémoire | next=Exemples de microarchitectures CPU : le cas du x86 | nextText=Exemples de microarchitectures CPU : le cas du x86 }} </noinclude> jn6flwzc70rvgve92a8460inb7tia1q 772759 772758 2026-09-21T21:42:18Z Mewtow 31375 /* Les CPU superscalaires à plusieurs unités spécialisées */ 772759 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. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps. 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. 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. Deux branchements consécutifs sont quant à eux plus fréquents, mais exécuter deux branchements en même temps pose un problème : que faire quand les deux sont pris ? Deux accès mémoire consécutifs est assez rare. La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs sont systématiquement supportées, car dupliquer des ALU 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 et 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'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. 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. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant. 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. ===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 4 instructions, compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément deux 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. 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. : 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 microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle. 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. ==L'évolution historique des processeurs superscalaires== L'évolution des CPU superscalaires historiques 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. 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. 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. Disons qu'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. Par exemple, 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. ===La double émission entière-flottante=== Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est d'émettre une micro-opération entière en même temps qu'une micro-opé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, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité. La plupart des premiers processeurs superscalaires étaient de ce type. Les exemples les plus notables sont les processeurs POWER 1 et ses dérivés comme le ''RISC Single Chip''. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle. [[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]] La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisations visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate]. ===L'émission parallèle des branchements=== Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur. 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|Emission multiple des opérations entières, implémentation courante]] Pour les branchements, deux solutions sont possibles. Avec la première, il y a émission parallèle des branchements, à savoir qu'il est possible d'émettre en branchement en parallèle de deux-trois instructions entières. Et cela impose la présence d'une unité de branchement séparée, avec son port d'émission. Avec la seconde, les branchements sont traités comme des instructions entières. Soit les branchements sont exécutés dans une ALU entière, soit il y a une unité de branchement reliée à un port d'émission pour les opérations entières. [[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Emission série et parallèle des branchements]] En général, les branchements sont exécutés comme des opérations entières. Cela permet d'économiser une unité de calcul et beaucoup de transistors dans l'unité d'émission/chargement/... {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 |- | * ALU entière * Multiplieur | * ALU entière * Barrel shifter * Unité de branchement | FPU |- | * Additions, soustractions, opérations bit à bit, etc. * Multiplication | * Additions, soustractions, opérations bit à bit, etc. * Décalages et rotations * Branchements |Opération flottante |} ===Les CPU superscalaires modernes=== Le premier CPU superscalaire grand public était le Pentium. Et c'était un CPU double émission qui n'implémentait pas la double émission entière-flottante, mais qui avait deux ALU entières. 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. Ils ont eux aussi privilégié l'ajout d'ALU entières. Les multiplieurs n'ont pas été dupliqués, sauf sur les processeurs superscalaires très larges. La raison est qu'il est rare de devoir exécuter deux multiplications à la fois. Et c'est la même chose pour le ''barrel shifter''. Pour donner quelques exemples, étudions les processeurs superscalaire PowerPC. Il y a eu plusieurs modèles, avec des jeux d'instructions légèrement différents, mais aussi et surtout des microarchitectures assez différentes. Afin de simplifier les explications, 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. Le PowerPC 603 utilisait une double émission "parfaite", avec émission parallèle des branchements. En clair, il pouvait émettre deux instructions + un branchement. Il intégrait une ALU entière, une FPU, l'unité de branchement et l'unité mémoire. Toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. Il avait donc une réelle double émission, ce qui fait qu'il était qualifié de ''CPU 2 + Branch''. Le PowerPC 604 est lui passé à la quadruple émission, avec émission parallèle des branchements. En clair, c'était un CPU de type ''4 + Branch''. 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, mais c'était le seul à être partagé. En clair, le CPU pouvait émettre deux INT + MEM + (soit MUL, soit FLOAT). Vint ensuite la génération suivante avec les PowerPC G3 et G4. Ils implémentaient la double émission + branchement. La différence avec le 603 est qu'ils intégraient deux ALU entières, pas une seule. Ils pouvaient donc exécuter deux opérations entières en même temps, la seule contrainte étant sur les multiplications (il n'y a qu'un seul multiplieur). Les combinaisons possibles étaient INT + INT , INT + MUL, INT + FLOAT, INT + MEM, FLOAT + MEM. La '''double émission flottante''' a été utilisée sur les processeurs Alpha et SPARC. L'idée est d'émettre deux opérations flottantes à la fois. Sauf qu'une implémentation naïve demanderait de dupliquer la FPU, ce qui aurait un cout en circuit rédhibitoire. Pour rappel, les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant. L'idée est d'avoir des ports d'émission séparés pour l'additionneur flottant et le multiplieur flottant. Le processeur peut alors émettre une addition flottante en même temps qu'une multiplication flottante. Les autres circuits de calcul flottant sont répartis sur ces deux ports d'émission flottants. L'avantage est que cela se marie bien avec l'usage d'une fenêtre d'instruction séparée pour les opérations flottantes. La fenêtre d'instruction a alors deux ports séparés, au lieu d'un seul. Rajouter un second port d'émission flottant n'est pas trop un problème, car le cout lié à l'ajout d'un port n'est pas linéaire. Passer de un port à deux a un cout tolérable, bien plus que de passer de 3 ports à 4 ou de 4 à 5. ==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 simultanné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=== 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. Elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. La ''load/store queue'' reste une structure unique, mais on peut lui rajouter des ports. Et à ce peit jeu, il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire. [[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]] 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. Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre. Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Le Pentium 4 était dans ce cas, mais il était un peu seul dans sa catégorie. Un cas similaire est celui où l'unité mémoire est alimentée par une fenêtre de micro-opération mélangeant instructions mémoire et entières. Les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. Dans ce cas, l'émission multiple est rarement implémentée. Le cas suivant a une file pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique. Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui pouvaient seulement émettre une lecture en même temps qu'une écriture. Ils avaient deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse. [[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]] Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture demande d'utiliser une file d'écriture multiport, ce qui est assez compliqué. Implémenter des lectures en plus est plus simple : il suffit de rajouter une seconde file de µops LOAD et une unité de calcul, alimentées via un second port d'émission. Il est aussi possible de rajouter un second port de lecture au cache, mais ce n'est pas obligatoire. Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse. De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports de lecture et d'écriture car il y a plus de lecture que d'écritures. Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. : Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse. [[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]] Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettent plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail. ==Les optimisations des processeurs superscalaires larges== Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul. ===L'étape de chargement d'un CPU superscalaire large=== Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 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 autoaligné.]] Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements ! [[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]] Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez 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 microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total). Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois 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 microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement. Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations. ===La micro-fusion des processeurs Intel et AMD=== La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''. Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''. Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout. La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Le parallélisme mémoire | prevText=Le parallélisme mémoire | next=Exemples de microarchitectures CPU : le cas du x86 | nextText=Exemples de microarchitectures CPU : le cas du x86 }} </noinclude> lw4qsv5x5wo34mega4rktszsph7tgk2 772760 772759 2026-09-21T21:46:37Z Mewtow 31375 /* Les CPU superscalaires modernes */ 772760 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. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps. 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. 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. Deux branchements consécutifs sont quant à eux plus fréquents, mais exécuter deux branchements en même temps pose un problème : que faire quand les deux sont pris ? Deux accès mémoire consécutifs est assez rare. La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs sont systématiquement supportées, car dupliquer des ALU 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 et 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'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. 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. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant. 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. ===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 4 instructions, compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément deux 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. 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. : 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 microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle. 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. ==L'évolution historique des processeurs superscalaires== L'évolution des CPU superscalaires historiques 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. 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. 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. Disons qu'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. Par exemple, 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. ===La double émission entière-flottante=== Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est d'émettre une micro-opération entière en même temps qu'une micro-opé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, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité. La plupart des premiers processeurs superscalaires étaient de ce type. Les exemples les plus notables sont les processeurs POWER 1 et ses dérivés comme le ''RISC Single Chip''. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle. [[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]] La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisations visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate]. ===L'émission parallèle des branchements=== Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur. 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|Emission multiple des opérations entières, implémentation courante]] Pour les branchements, deux solutions sont possibles. Avec la première, il y a émission parallèle des branchements, à savoir qu'il est possible d'émettre en branchement en parallèle de deux-trois instructions entières. Et cela impose la présence d'une unité de branchement séparée, avec son port d'émission. Avec la seconde, les branchements sont traités comme des instructions entières. Soit les branchements sont exécutés dans une ALU entière, soit il y a une unité de branchement reliée à un port d'émission pour les opérations entières. [[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Emission série et parallèle des branchements]] En général, les branchements sont exécutés comme des opérations entières. Cela permet d'économiser une unité de calcul et beaucoup de transistors dans l'unité d'émission/chargement/... {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 |- | * ALU entière * Multiplieur | * ALU entière * Barrel shifter * Unité de branchement | FPU |- | * Additions, soustractions, opérations bit à bit, etc. * Multiplication | * Additions, soustractions, opérations bit à bit, etc. * Décalages et rotations * Branchements |Opération flottante |} ===Les CPU superscalaires modernes=== Le premier CPU superscalaire grand public était le Pentium. Et c'était un CPU double émission qui n'implémentait pas la double émission entière-flottante, mais qui avait deux ALU entières. 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. Ils ont eux aussi privilégié l'ajout d'ALU entières. Les multiplieurs n'ont pas été dupliqués, sauf sur les processeurs superscalaires très larges, car il est rare de devoir exécuter deux multiplications à la fois. Et c'est la même chose pour le ''barrel shifter''. La FPU aussi n'a pas été 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 donner quelques exemples, étudions les processeurs superscalaire PowerPC. Il y a eu plusieurs modèles, avec des jeux d'instructions légèrement différents, mais aussi et surtout des microarchitectures assez différentes. Afin de simplifier les explications, 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. Le PowerPC 603 utilisait une double émission "parfaite", avec émission parallèle des branchements. En clair, il pouvait émettre deux instructions + un branchement. Il intégrait une ALU entière, une FPU, l'unité de branchement et l'unité mémoire. Toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. Il avait donc une réelle double émission, ce qui fait qu'il était qualifié de ''CPU 2 + Branch''. Le PowerPC 604 est lui passé à la quadruple émission, avec émission parallèle des branchements. En clair, c'était un CPU de type ''4 + Branch''. 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, mais c'était le seul à être partagé. En clair, le CPU pouvait émettre deux INT + MEM + (soit MUL, soit FLOAT). Vint ensuite la génération suivante avec les PowerPC G3 et G4. Ils implémentaient la double émission + branchement. La différence avec le 603 est qu'ils intégraient deux ALU entières, pas une seule. Ils pouvaient donc exécuter deux opérations entières en même temps, la seule contrainte étant sur les multiplications (il n'y a qu'un seul multiplieur). Les combinaisons possibles étaient INT + INT , INT + MUL, INT + FLOAT, INT + MEM, FLOAT + MEM. ==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 simultanné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=== 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. Elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. La ''load/store queue'' reste une structure unique, mais on peut lui rajouter des ports. Et à ce peit jeu, il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire. [[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]] 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. Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre. Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Le Pentium 4 était dans ce cas, mais il était un peu seul dans sa catégorie. Un cas similaire est celui où l'unité mémoire est alimentée par une fenêtre de micro-opération mélangeant instructions mémoire et entières. Les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. Dans ce cas, l'émission multiple est rarement implémentée. Le cas suivant a une file pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique. Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui pouvaient seulement émettre une lecture en même temps qu'une écriture. Ils avaient deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse. [[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]] Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture demande d'utiliser une file d'écriture multiport, ce qui est assez compliqué. Implémenter des lectures en plus est plus simple : il suffit de rajouter une seconde file de µops LOAD et une unité de calcul, alimentées via un second port d'émission. Il est aussi possible de rajouter un second port de lecture au cache, mais ce n'est pas obligatoire. Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse. De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports de lecture et d'écriture car il y a plus de lecture que d'écritures. Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. : Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse. [[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]] Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettent plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail. ==Les optimisations des processeurs superscalaires larges== Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul. ===L'étape de chargement d'un CPU superscalaire large=== Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 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 autoaligné.]] Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements ! [[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]] Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez 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 microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total). Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois 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 microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement. Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations. ===La micro-fusion des processeurs Intel et AMD=== La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''. Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''. Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout. La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Le parallélisme mémoire | prevText=Le parallélisme mémoire | next=Exemples de microarchitectures CPU : le cas du x86 | nextText=Exemples de microarchitectures CPU : le cas du x86 }} </noinclude> ay63ubsjn7r43d0qkk8o81n31xclbdg 772761 772760 2026-09-21T21:47:53Z Mewtow 31375 /* Les CPU superscalaires modernes */ 772761 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. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps. 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. 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. Deux branchements consécutifs sont quant à eux plus fréquents, mais exécuter deux branchements en même temps pose un problème : que faire quand les deux sont pris ? Deux accès mémoire consécutifs est assez rare. La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs sont systématiquement supportées, car dupliquer des ALU 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 et 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'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. 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. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant. 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. ===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 4 instructions, compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément deux 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. 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. : 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 microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle. 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. ==L'évolution historique des processeurs superscalaires== L'évolution des CPU superscalaires historiques 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. 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. 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. Disons qu'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. Par exemple, 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. ===La double émission entière-flottante=== Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est d'émettre une micro-opération entière en même temps qu'une micro-opé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, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité. La plupart des premiers processeurs superscalaires étaient de ce type. Les exemples les plus notables sont les processeurs POWER 1 et ses dérivés comme le ''RISC Single Chip''. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle. [[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]] La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisations visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate]. ===L'émission parallèle des branchements=== Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur. 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|Emission multiple des opérations entières, implémentation courante]] Pour les branchements, deux solutions sont possibles. Avec la première, il y a émission parallèle des branchements, à savoir qu'il est possible d'émettre en branchement en parallèle de deux-trois instructions entières. Et cela impose la présence d'une unité de branchement séparée, avec son port d'émission. Avec la seconde, les branchements sont traités comme des instructions entières. Soit les branchements sont exécutés dans une ALU entière, soit il y a une unité de branchement reliée à un port d'émission pour les opérations entières. [[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Emission série et parallèle des branchements]] En général, les branchements sont exécutés comme des opérations entières. Cela permet d'économiser une unité de calcul et beaucoup de transistors dans l'unité d'émission/chargement/... {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 |- | * ALU entière * Multiplieur | * ALU entière * Barrel shifter * Unité de branchement | FPU |- | * Additions, soustractions, opérations bit à bit, etc. * Multiplication | * Additions, soustractions, opérations bit à bit, etc. * Décalages et rotations * Branchements |Opération flottante |} ===Les CPU superscalaires modernes=== Le premier CPU superscalaire grand public était le Pentium. Et c'était un CPU double émission qui n'implémentait pas la double émission entière-flottante, mais qui avait deux ALU entières. 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. Les CPU superscalaires modernes privilégient l'ajout d'ALU entières. Les multiplieurs n'ont pas été dupliqués, sauf sur les processeurs superscalaires très larges, car il est rare de devoir exécuter deux multiplications à la fois. Et c'est la même chose pour le ''barrel shifter''. La FPU aussi n'a pas été 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 donner quelques exemples, étudions les processeurs superscalaire PowerPC. Il y a eu plusieurs modèles, avec des jeux d'instructions légèrement différents, mais aussi et surtout des microarchitectures assez différentes. Afin de simplifier les explications, 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. Le PowerPC 603 utilisait une double émission "parfaite", avec émission parallèle des branchements. En clair, il pouvait émettre deux instructions + un branchement. Il intégrait une ALU entière, une FPU, l'unité de branchement et l'unité mémoire. Toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. Il avait donc une réelle double émission, ce qui fait qu'il était qualifié de ''CPU 2 + Branch''. Le PowerPC 604 est lui passé à la quadruple émission, avec émission parallèle des branchements. En clair, c'était un CPU de type ''4 + Branch''. 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, mais c'était le seul à être partagé. En clair, le CPU pouvait émettre deux INT + MEM + (soit MUL, soit FLOAT). Vint ensuite la génération suivante avec les PowerPC G3 et G4. Ils implémentaient la double émission + branchement. La différence avec le 603 est qu'ils intégraient deux ALU entières, pas une seule. Ils pouvaient donc exécuter deux opérations entières en même temps, la seule contrainte étant sur les multiplications (il n'y a qu'un seul multiplieur). Les combinaisons possibles étaient INT + INT , INT + MUL, INT + FLOAT, INT + MEM, FLOAT + MEM. ==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 simultanné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=== 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. Elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. La ''load/store queue'' reste une structure unique, mais on peut lui rajouter des ports. Et à ce peit jeu, il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire. [[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]] 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. Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre. Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Le Pentium 4 était dans ce cas, mais il était un peu seul dans sa catégorie. Un cas similaire est celui où l'unité mémoire est alimentée par une fenêtre de micro-opération mélangeant instructions mémoire et entières. Les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. Dans ce cas, l'émission multiple est rarement implémentée. Le cas suivant a une file pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique. Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui pouvaient seulement émettre une lecture en même temps qu'une écriture. Ils avaient deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse. [[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]] Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture demande d'utiliser une file d'écriture multiport, ce qui est assez compliqué. Implémenter des lectures en plus est plus simple : il suffit de rajouter une seconde file de µops LOAD et une unité de calcul, alimentées via un second port d'émission. Il est aussi possible de rajouter un second port de lecture au cache, mais ce n'est pas obligatoire. Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse. De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports de lecture et d'écriture car il y a plus de lecture que d'écritures. Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. : Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse. [[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]] Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettent plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail. ==Les optimisations des processeurs superscalaires larges== Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul. ===L'étape de chargement d'un CPU superscalaire large=== Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 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 autoaligné.]] Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements ! [[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]] Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez 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 microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total). Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois 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 microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement. Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations. ===La micro-fusion des processeurs Intel et AMD=== La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''. Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''. Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout. La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Le parallélisme mémoire | prevText=Le parallélisme mémoire | next=Exemples de microarchitectures CPU : le cas du x86 | nextText=Exemples de microarchitectures CPU : le cas du x86 }} </noinclude> 3ie2ow8inadnqk7835vi7ckt4ibko61 772762 772761 2026-09-21T21:49:57Z Mewtow 31375 /* Les CPU superscalaires modernes */ 772762 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. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps. 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. 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. Deux branchements consécutifs sont quant à eux plus fréquents, mais exécuter deux branchements en même temps pose un problème : que faire quand les deux sont pris ? Deux accès mémoire consécutifs est assez rare. La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs sont systématiquement supportées, car dupliquer des ALU 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 et 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'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. 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. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant. 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. ===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 4 instructions, compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément deux 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. 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. : 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 microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle. 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. ==L'évolution historique des processeurs superscalaires== L'évolution des CPU superscalaires historiques 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. 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. 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. Disons qu'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. Par exemple, 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. ===La double émission entière-flottante=== Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est d'émettre une micro-opération entière en même temps qu'une micro-opé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, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité. La plupart des premiers processeurs superscalaires étaient de ce type. Les exemples les plus notables sont les processeurs POWER 1 et ses dérivés comme le ''RISC Single Chip''. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle. [[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]] La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisations visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate]. ===L'émission parallèle des branchements=== Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur. 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|Emission multiple des opérations entières, implémentation courante]] Pour les branchements, deux solutions sont possibles. Avec la première, il y a émission parallèle des branchements, à savoir qu'il est possible d'émettre en branchement en parallèle de deux-trois instructions entières. Et cela impose la présence d'une unité de branchement séparée, avec son port d'émission. Avec la seconde, les branchements sont traités comme des instructions entières. Soit les branchements sont exécutés dans une ALU entière, soit il y a une unité de branchement reliée à un port d'émission pour les opérations entières. [[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Emission série et parallèle des branchements]] En général, les branchements sont exécutés comme des opérations entières. Cela permet d'économiser une unité de calcul et beaucoup de transistors dans l'unité d'émission/chargement/... {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 |- | * ALU entière * Multiplieur | * ALU entière * Barrel shifter * Unité de branchement | FPU |- | * Additions, soustractions, opérations bit à bit, etc. * Multiplication | * Additions, soustractions, opérations bit à bit, etc. * Décalages et rotations * Branchements |Opération flottante |} ===Les CPU superscalaires modernes=== Le premier CPU superscalaire grand public était le Pentium. Et c'était un CPU double émission qui n'implémentait pas la double émission entière-flottante, mais qui avait deux ALU entières. 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. Les CPU superscalaires modernes privilégient l'ajout d'ALU entières. Les multiplieurs n'ont pas été dupliqués, sauf sur les processeurs superscalaires très larges, car il est rare de devoir exécuter deux multiplications à la fois. Et c'est la même chose pour le ''barrel shifter''. La FPU aussi n'a pas été 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 donner quelques exemples, étudions les processeurs superscalaire PowerPC. Il y a eu plusieurs modèles, avec des jeux d'instructions légèrement différents, mais aussi et surtout des microarchitectures assez différentes. Afin de simplifier les explications, 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. Le PowerPC 603 utilisait une double émission "parfaite", avec émission parallèle des branchements. En clair, il pouvait émettre deux instructions + un branchement. Il intégrait une ALU entière, une FPU, l'unité de branchement et l'unité mémoire. Toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. Il avait donc une réelle double émission, ce qui fait qu'il était qualifié de ''CPU 2 + Branch''. Le PowerPC 604 est lui passé à la quadruple émission, avec émission parallèle des branchements. En clair, c'était un CPU de type ''4 + Branch''. 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, mais c'était le seul à être partagé. En clair, le CPU pouvait émettre deux INT + MEM + (soit MUL, soit FLOAT). Vint ensuite la génération suivante avec les PowerPC G3 et G4. Ils implémentaient la double émission + branchement. La différence avec le 603 est qu'ils intégraient deux ALU entières, pas une seule. Ils pouvaient donc exécuter deux opérations entières en même temps, la seule contrainte étant sur les multiplications (il n'y a qu'un seul multiplieur). Les combinaisons possibles étaient INT + INT , INT + MUL, INT + FLOAT, INT + MEM, FLOAT + MEM. ==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 simultanné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=== 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. Elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. La ''load/store queue'' reste une structure unique, mais on peut lui rajouter des ports. Et à ce peit jeu, il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire. [[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]] 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. Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre. Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Le Pentium 4 était dans ce cas, mais il était un peu seul dans sa catégorie. Un cas similaire est celui où l'unité mémoire est alimentée par une fenêtre de micro-opération mélangeant instructions mémoire et entières. Les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. Dans ce cas, l'émission multiple est rarement implémentée. Le cas suivant a une file pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique. Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui pouvaient seulement émettre une lecture en même temps qu'une écriture. Ils avaient deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse. [[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]] Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture demande d'utiliser une file d'écriture multiport, ce qui est assez compliqué. Implémenter des lectures en plus est plus simple : il suffit de rajouter une seconde file de µops LOAD et une unité de calcul, alimentées via un second port d'émission. Il est aussi possible de rajouter un second port de lecture au cache, mais ce n'est pas obligatoire. Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse. De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports de lecture et d'écriture car il y a plus de lecture que d'écritures. Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. : Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse. [[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]] Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettent plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail. ==Les optimisations des processeurs superscalaires larges== Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul. ===L'étape de chargement d'un CPU superscalaire large=== Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 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 autoaligné.]] Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements ! [[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]] Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez 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 microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total). Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois 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 microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement. Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations. ===La micro-fusion des processeurs Intel et AMD=== La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''. Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''. Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout. La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Le parallélisme mémoire | prevText=Le parallélisme mémoire | next=Exemples de microarchitectures CPU : le cas du x86 | nextText=Exemples de microarchitectures CPU : le cas du x86 }} </noinclude> h4dsgtr2aywiifut8wmjozycm6m6sdi 772763 772762 2026-09-21T21:50:55Z Mewtow 31375 /* Les CPU superscalaires modernes */ 772763 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. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps. 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. 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. Deux branchements consécutifs sont quant à eux plus fréquents, mais exécuter deux branchements en même temps pose un problème : que faire quand les deux sont pris ? Deux accès mémoire consécutifs est assez rare. La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs sont systématiquement supportées, car dupliquer des ALU 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 et 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'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. 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. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant. 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. ===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 4 instructions, compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément deux 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. 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. : 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 microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle. 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. ==L'évolution historique des processeurs superscalaires== L'évolution des CPU superscalaires historiques 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. 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. 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. Disons qu'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. Par exemple, 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. ===La double émission entière-flottante=== Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est d'émettre une micro-opération entière en même temps qu'une micro-opé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, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité. La plupart des premiers processeurs superscalaires étaient de ce type. Les exemples les plus notables sont les processeurs POWER 1 et ses dérivés comme le ''RISC Single Chip''. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle. [[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]] La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisations visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate]. ===L'émission parallèle des branchements=== Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur. 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|Emission multiple des opérations entières, implémentation courante]] Pour les branchements, deux solutions sont possibles. Avec la première, il y a émission parallèle des branchements, à savoir qu'il est possible d'émettre en branchement en parallèle de deux-trois instructions entières. Et cela impose la présence d'une unité de branchement séparée, avec son port d'émission. Avec la seconde, les branchements sont traités comme des instructions entières. Soit les branchements sont exécutés dans une ALU entière, soit il y a une unité de branchement reliée à un port d'émission pour les opérations entières. [[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Emission série et parallèle des branchements]] En général, les branchements sont exécutés comme des opérations entières. Cela permet d'économiser une unité de calcul et beaucoup de transistors dans l'unité d'émission/chargement/... {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 |- | * ALU entière * Multiplieur | * ALU entière * Barrel shifter * Unité de branchement | FPU |- | * Additions, soustractions, opérations bit à bit, etc. * Multiplication | * Additions, soustractions, opérations bit à bit, etc. * Décalages et rotations * Branchements |Opération flottante |} ===Les CPU superscalaires modernes=== Le premier CPU superscalaire grand public était le Pentium. Et c'était un CPU double émission qui n'implémentait pas la double émission entière-flottante, mais qui avait deux ALU entières. 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. Les CPU superscalaires modernes privilégient l'ajout d'ALU entières. Les multiplieurs n'ont pas été dupliqués, sauf sur les processeurs superscalaires très larges, car il est rare de devoir exécuter deux multiplications à la fois. Et c'est la même chose pour le ''barrel shifter''. La FPU aussi n'a pas été 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 donner quelques exemples, étudions les processeurs superscalaire PowerPC. Il y a eu plusieurs modèles, avec des jeux d'instructions légèrement différents, mais aussi et surtout des microarchitectures assez différentes. Afin de simplifier les explications, 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. Le PowerPC 603 utilisait une double émission "parfaite", avec émission parallèle des branchements. En clair, il pouvait émettre deux instructions + un branchement. Il intégrait une ALU entière, une FPU, l'unité de branchement et l'unité mémoire. Toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. Il avait donc une réelle double émission, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''. Le PowerPC 604 est lui passé à la quadruple émission, avec émission parallèle des branchements. En clair, c'était un CPU de type ''4-way + Branch''. 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, mais c'était le seul à être partagé. En clair, le CPU pouvait émettre deux INT + MEM + (soit MUL, soit FLOAT). Vint ensuite la génération suivante avec les PowerPC G3 et G4. Ils implémentaient la double émission + branchement. La différence avec le 603 est qu'ils intégraient deux ALU entières, pas une seule. Ils pouvaient donc exécuter deux opérations entières en même temps, la seule contrainte étant sur les multiplications (il n'y a qu'un seul multiplieur). Les combinaisons possibles étaient INT + INT , INT + MUL, INT + FLOAT, INT + MEM, FLOAT + MEM. ==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 simultanné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=== 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. Elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. La ''load/store queue'' reste une structure unique, mais on peut lui rajouter des ports. Et à ce peit jeu, il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire. [[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]] 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. Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre. Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Le Pentium 4 était dans ce cas, mais il était un peu seul dans sa catégorie. Un cas similaire est celui où l'unité mémoire est alimentée par une fenêtre de micro-opération mélangeant instructions mémoire et entières. Les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. Dans ce cas, l'émission multiple est rarement implémentée. Le cas suivant a une file pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique. Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui pouvaient seulement émettre une lecture en même temps qu'une écriture. Ils avaient deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse. [[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]] Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture demande d'utiliser une file d'écriture multiport, ce qui est assez compliqué. Implémenter des lectures en plus est plus simple : il suffit de rajouter une seconde file de µops LOAD et une unité de calcul, alimentées via un second port d'émission. Il est aussi possible de rajouter un second port de lecture au cache, mais ce n'est pas obligatoire. Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse. De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports de lecture et d'écriture car il y a plus de lecture que d'écritures. Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. : Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse. [[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]] Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettent plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail. ==Les optimisations des processeurs superscalaires larges== Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul. ===L'étape de chargement d'un CPU superscalaire large=== Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 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 autoaligné.]] Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements ! [[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]] Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez 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 microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total). Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois 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 microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement. Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations. ===La micro-fusion des processeurs Intel et AMD=== La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''. Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''. Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout. La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Le parallélisme mémoire | prevText=Le parallélisme mémoire | next=Exemples de microarchitectures CPU : le cas du x86 | nextText=Exemples de microarchitectures CPU : le cas du x86 }} </noinclude> hvrwlzwax4d6qqwpf5mnj6wu7iolqa2 772764 772763 2026-09-21T21:51:57Z Mewtow 31375 /* Les CPU superscalaires modernes */ 772764 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. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps. 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. 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. Deux branchements consécutifs sont quant à eux plus fréquents, mais exécuter deux branchements en même temps pose un problème : que faire quand les deux sont pris ? Deux accès mémoire consécutifs est assez rare. La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs sont systématiquement supportées, car dupliquer des ALU 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 et 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'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. 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. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant. 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. ===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 4 instructions, compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément deux 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. 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. : 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 microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle. 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. ==L'évolution historique des processeurs superscalaires== L'évolution des CPU superscalaires historiques 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. 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. 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. Disons qu'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. Par exemple, 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. ===La double émission entière-flottante=== Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est d'émettre une micro-opération entière en même temps qu'une micro-opé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, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité. La plupart des premiers processeurs superscalaires étaient de ce type. Les exemples les plus notables sont les processeurs POWER 1 et ses dérivés comme le ''RISC Single Chip''. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle. [[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]] La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisations visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate]. ===L'émission parallèle des branchements=== Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur. 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|Emission multiple des opérations entières, implémentation courante]] Pour les branchements, deux solutions sont possibles. Avec la première, il y a émission parallèle des branchements, à savoir qu'il est possible d'émettre en branchement en parallèle de deux-trois instructions entières. Et cela impose la présence d'une unité de branchement séparée, avec son port d'émission. Avec la seconde, les branchements sont traités comme des instructions entières. Soit les branchements sont exécutés dans une ALU entière, soit il y a une unité de branchement reliée à un port d'émission pour les opérations entières. [[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Emission série et parallèle des branchements]] En général, les branchements sont exécutés comme des opérations entières. Cela permet d'économiser une unité de calcul et beaucoup de transistors dans l'unité d'émission/chargement/... {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 |- | * ALU entière * Multiplieur | * ALU entière * Barrel shifter * Unité de branchement | FPU |- | * Additions, soustractions, opérations bit à bit, etc. * Multiplication | * Additions, soustractions, opérations bit à bit, etc. * Décalages et rotations * Branchements |Opération flottante |} ===Les CPU superscalaires modernes=== Le premier CPU superscalaire grand public était le Pentium. Et c'était un CPU double émission qui n'implémentait pas la double émission entière-flottante, mais qui avait deux ALU entières. 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. Les CPU superscalaires modernes privilégient l'ajout d'ALU entières. Les multiplieurs n'ont pas été dupliqués, sauf sur les processeurs superscalaires très larges, car il est rare de devoir exécuter deux multiplications à la fois. Et c'est la même chose pour le ''barrel shifter''. La FPU aussi n'a pas été 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 donner quelques exemples, étudions les processeurs superscalaire PowerPC. Il y a eu plusieurs modèles, avec des jeux d'instructions légèrement différents, mais aussi et surtout des microarchitectures assez différentes. Le PowerPC 603 utilisait une double émission "parfaite", avec émission parallèle des branchements. En clair, il pouvait émettre deux instructions + un branchement. Il intégrait une ALU entière, une FPU, l'unité de branchement et l'unité mémoire. Toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. Il avait donc une réelle double émission, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''. Le PowerPC 604 est lui passé à la quadruple émission, avec émission parallèle des branchements. En clair, c'était un CPU de type ''4-way + Branch''. 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, mais c'était le seul à être partagé. En clair, le CPU pouvait émettre deux INT + MEM + (soit MUL, soit FLOAT). Vint ensuite la génération suivante avec les PowerPC G3 et G4. Ils implémentaient la double émission + branchement. La différence avec le 603 est qu'ils intégraient deux ALU entières, pas une seule. Ils pouvaient donc exécuter deux opérations entières en même temps, la seule contrainte étant sur les multiplications (il n'y a qu'un seul multiplieur). Les combinaisons possibles étaient INT + INT , INT + MUL, INT + FLOAT, INT + MEM, FLOAT + MEM. ==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 simultanné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=== 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. Elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. La ''load/store queue'' reste une structure unique, mais on peut lui rajouter des ports. Et à ce peit jeu, il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire. [[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]] 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. Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre. Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Le Pentium 4 était dans ce cas, mais il était un peu seul dans sa catégorie. Un cas similaire est celui où l'unité mémoire est alimentée par une fenêtre de micro-opération mélangeant instructions mémoire et entières. Les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. Dans ce cas, l'émission multiple est rarement implémentée. Le cas suivant a une file pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique. Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui pouvaient seulement émettre une lecture en même temps qu'une écriture. Ils avaient deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse. [[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]] Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture demande d'utiliser une file d'écriture multiport, ce qui est assez compliqué. Implémenter des lectures en plus est plus simple : il suffit de rajouter une seconde file de µops LOAD et une unité de calcul, alimentées via un second port d'émission. Il est aussi possible de rajouter un second port de lecture au cache, mais ce n'est pas obligatoire. Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse. De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports de lecture et d'écriture car il y a plus de lecture que d'écritures. Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. : Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse. [[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]] Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettent plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail. ==Les optimisations des processeurs superscalaires larges== Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul. ===L'étape de chargement d'un CPU superscalaire large=== Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 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 autoaligné.]] Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements ! [[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]] Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez 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 microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total). Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois 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 microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement. Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations. ===La micro-fusion des processeurs Intel et AMD=== La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''. Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''. Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout. La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Le parallélisme mémoire | prevText=Le parallélisme mémoire | next=Exemples de microarchitectures CPU : le cas du x86 | nextText=Exemples de microarchitectures CPU : le cas du x86 }} </noinclude> nhu6ax1z5qor7fh75b5itb7fgfgny9d 772765 772764 2026-09-21T21:54:29Z Mewtow 31375 /* Les contraintes d’appariement */ 772765 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. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps. 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. 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. Deux branchements consécutifs sont quant à eux plus fréquents, mais exécuter deux branchements en même temps pose un problème : que faire quand les deux sont pris ? Deux accès mémoire consécutifs est assez rare. La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs sont systématiquement supportées, car dupliquer des ALU 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 et 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 possibels 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'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. 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. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant. 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. ===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 4 instructions, compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément deux 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. 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. : 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 microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle. 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. ==L'évolution historique des processeurs superscalaires== L'évolution des CPU superscalaires historiques 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. 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. 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. Disons qu'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. Par exemple, 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. ===La double émission entière-flottante=== Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est d'émettre une micro-opération entière en même temps qu'une micro-opé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, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité. La plupart des premiers processeurs superscalaires étaient de ce type. Les exemples les plus notables sont les processeurs POWER 1 et ses dérivés comme le ''RISC Single Chip''. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle. [[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]] La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisations visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate]. ===L'émission parallèle des branchements=== Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur. 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|Emission multiple des opérations entières, implémentation courante]] Pour les branchements, deux solutions sont possibles. Avec la première, il y a émission parallèle des branchements, à savoir qu'il est possible d'émettre en branchement en parallèle de deux-trois instructions entières. Et cela impose la présence d'une unité de branchement séparée, avec son port d'émission. Avec la seconde, les branchements sont traités comme des instructions entières. Soit les branchements sont exécutés dans une ALU entière, soit il y a une unité de branchement reliée à un port d'émission pour les opérations entières. [[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Emission série et parallèle des branchements]] En général, les branchements sont exécutés comme des opérations entières. Cela permet d'économiser une unité de calcul et beaucoup de transistors dans l'unité d'émission/chargement/... {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 |- | * ALU entière * Multiplieur | * ALU entière * Barrel shifter * Unité de branchement | FPU |- | * Additions, soustractions, opérations bit à bit, etc. * Multiplication | * Additions, soustractions, opérations bit à bit, etc. * Décalages et rotations * Branchements |Opération flottante |} ===Les CPU superscalaires modernes=== Le premier CPU superscalaire grand public était le Pentium. Et c'était un CPU double émission qui n'implémentait pas la double émission entière-flottante, mais qui avait deux ALU entières. 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. Les CPU superscalaires modernes privilégient l'ajout d'ALU entières. Les multiplieurs n'ont pas été dupliqués, sauf sur les processeurs superscalaires très larges, car il est rare de devoir exécuter deux multiplications à la fois. Et c'est la même chose pour le ''barrel shifter''. La FPU aussi n'a pas été 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 donner quelques exemples, étudions les processeurs superscalaire PowerPC. Il y a eu plusieurs modèles, avec des jeux d'instructions légèrement différents, mais aussi et surtout des microarchitectures assez différentes. Le PowerPC 603 utilisait une double émission "parfaite", avec émission parallèle des branchements. En clair, il pouvait émettre deux instructions + un branchement. Il intégrait une ALU entière, une FPU, l'unité de branchement et l'unité mémoire. Toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. Il avait donc une réelle double émission, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''. Le PowerPC 604 est lui passé à la quadruple émission, avec émission parallèle des branchements. En clair, c'était un CPU de type ''4-way + Branch''. 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, mais c'était le seul à être partagé. En clair, le CPU pouvait émettre deux INT + MEM + (soit MUL, soit FLOAT). Vint ensuite la génération suivante avec les PowerPC G3 et G4. Ils implémentaient la double émission + branchement. La différence avec le 603 est qu'ils intégraient deux ALU entières, pas une seule. Ils pouvaient donc exécuter deux opérations entières en même temps, la seule contrainte étant sur les multiplications (il n'y a qu'un seul multiplieur). Les combinaisons possibles étaient INT + INT , INT + MUL, INT + FLOAT, INT + MEM, FLOAT + MEM. ==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 simultanné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=== 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. Elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. La ''load/store queue'' reste une structure unique, mais on peut lui rajouter des ports. Et à ce peit jeu, il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire. [[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]] 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. Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre. Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Le Pentium 4 était dans ce cas, mais il était un peu seul dans sa catégorie. Un cas similaire est celui où l'unité mémoire est alimentée par une fenêtre de micro-opération mélangeant instructions mémoire et entières. Les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. Dans ce cas, l'émission multiple est rarement implémentée. Le cas suivant a une file pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique. Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui pouvaient seulement émettre une lecture en même temps qu'une écriture. Ils avaient deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse. [[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]] Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture demande d'utiliser une file d'écriture multiport, ce qui est assez compliqué. Implémenter des lectures en plus est plus simple : il suffit de rajouter une seconde file de µops LOAD et une unité de calcul, alimentées via un second port d'émission. Il est aussi possible de rajouter un second port de lecture au cache, mais ce n'est pas obligatoire. Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse. De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports de lecture et d'écriture car il y a plus de lecture que d'écritures. Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. : Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse. [[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]] Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettent plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail. ==Les optimisations des processeurs superscalaires larges== Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul. ===L'étape de chargement d'un CPU superscalaire large=== Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 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 autoaligné.]] Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements ! [[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]] Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez 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 microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total). Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois 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 microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement. Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations. ===La micro-fusion des processeurs Intel et AMD=== La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''. Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''. Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout. La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Le parallélisme mémoire | prevText=Le parallélisme mémoire | next=Exemples de microarchitectures CPU : le cas du x86 | nextText=Exemples de microarchitectures CPU : le cas du x86 }} </noinclude> bf7sqff6t31lhxyhloj6rsqa490yage 772766 772765 2026-09-21T21:57:44Z Mewtow 31375 /* Les CPU superscalaires modernes */ 772766 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. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps. 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. 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. Deux branchements consécutifs sont quant à eux plus fréquents, mais exécuter deux branchements en même temps pose un problème : que faire quand les deux sont pris ? Deux accès mémoire consécutifs est assez rare. La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs sont systématiquement supportées, car dupliquer des ALU 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 et 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 possibels 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'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. 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. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant. 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. ===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 4 instructions, compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément deux 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. 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. : 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 microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle. 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. ==L'évolution historique des processeurs superscalaires== L'évolution des CPU superscalaires historiques 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. 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. 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. Disons qu'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. Par exemple, 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. ===La double émission entière-flottante=== Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est d'émettre une micro-opération entière en même temps qu'une micro-opé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, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité. La plupart des premiers processeurs superscalaires étaient de ce type. Les exemples les plus notables sont les processeurs POWER 1 et ses dérivés comme le ''RISC Single Chip''. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle. [[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]] La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisations visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate]. ===L'émission parallèle des branchements=== Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur. 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|Emission multiple des opérations entières, implémentation courante]] Pour les branchements, deux solutions sont possibles. Avec la première, il y a émission parallèle des branchements, à savoir qu'il est possible d'émettre en branchement en parallèle de deux-trois instructions entières. Et cela impose la présence d'une unité de branchement séparée, avec son port d'émission. Avec la seconde, les branchements sont traités comme des instructions entières. Soit les branchements sont exécutés dans une ALU entière, soit il y a une unité de branchement reliée à un port d'émission pour les opérations entières. [[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Emission série et parallèle des branchements]] En général, les branchements sont exécutés comme des opérations entières. Cela permet d'économiser une unité de calcul et beaucoup de transistors dans l'unité d'émission/chargement/... {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 |- | * ALU entière * Multiplieur | * ALU entière * Barrel shifter * Unité de branchement | FPU |- | * Additions, soustractions, opérations bit à bit, etc. * Multiplication | * Additions, soustractions, opérations bit à bit, etc. * Décalages et rotations * Branchements |Opération flottante |} ===Les CPU superscalaires modernes=== Le premier CPU superscalaire grand public était le Pentium. Et c'était un CPU double émission qui n'implémentait pas la double émission entière-flottante, mais qui avait deux ALU entières. 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. Les CPU superscalaires modernes privilégient l'ajout d'ALU entières. Les multiplieurs n'ont pas été dupliqués, sauf sur les processeurs superscalaires très larges, car il est rare de devoir exécuter deux multiplications à la fois. Et c'est la même chose pour le ''barrel shifter''. La FPU aussi n'a pas été 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 donner quelques exemples, étudions les processeurs superscalaire PowerPC. Il y a eu plusieurs modèles, avec des jeux d'instructions légèrement différents, mais aussi et surtout des microarchitectures assez différentes. Le PowerPC 604 est lui passé à la quadruple émission, avec émission parallèle des branchements. En clair, c'était un CPU de type ''4-way + Branch''. 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, mais c'était le seul à être partagé. En clair, le CPU pouvait émettre deux INT + MEM + (soit MUL, soit FLOAT). Vint ensuite la génération suivante avec les PowerPC G3 et G4. Ils implémentaient la double émission + branchement. La différence avec le 603 est qu'ils intégraient deux ALU entières, pas une seule. Ils pouvaient donc exécuter deux opérations entières en même temps, la seule contrainte étant sur les multiplications (il n'y a qu'un seul multiplieur). Les combinaisons possibles étaient INT + INT , INT + MUL, INT + FLOAT, INT + MEM, FLOAT + MEM. ==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 simultanné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=== 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. Elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. La ''load/store queue'' reste une structure unique, mais on peut lui rajouter des ports. Et à ce peit jeu, il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire. [[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]] 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. Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre. Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Le Pentium 4 était dans ce cas, mais il était un peu seul dans sa catégorie. Un cas similaire est celui où l'unité mémoire est alimentée par une fenêtre de micro-opération mélangeant instructions mémoire et entières. Les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. Dans ce cas, l'émission multiple est rarement implémentée. Le cas suivant a une file pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique. Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui pouvaient seulement émettre une lecture en même temps qu'une écriture. Ils avaient deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse. [[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]] Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture demande d'utiliser une file d'écriture multiport, ce qui est assez compliqué. Implémenter des lectures en plus est plus simple : il suffit de rajouter une seconde file de µops LOAD et une unité de calcul, alimentées via un second port d'émission. Il est aussi possible de rajouter un second port de lecture au cache, mais ce n'est pas obligatoire. Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse. De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports de lecture et d'écriture car il y a plus de lecture que d'écritures. Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. : Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse. [[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]] Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettent plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail. ==Les optimisations des processeurs superscalaires larges== Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul. ===L'étape de chargement d'un CPU superscalaire large=== Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 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 autoaligné.]] Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements ! [[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]] Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez 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 microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total). Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois 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 microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement. Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations. ===La micro-fusion des processeurs Intel et AMD=== La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''. Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''. Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout. La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Le parallélisme mémoire | prevText=Le parallélisme mémoire | next=Exemples de microarchitectures CPU : le cas du x86 | nextText=Exemples de microarchitectures CPU : le cas du x86 }} </noinclude> 790shumkhhork1kfmrmi0h9d0tlufsd 772767 772766 2026-09-21T22:00:27Z Mewtow 31375 /* Les unités de calcul d'un processeur superscalaire */ 772767 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. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps. 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. 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. Deux branchements consécutifs sont quant à eux plus fréquents, mais exécuter deux branchements en même temps pose un problème : que faire quand les deux sont pris ? Deux accès mémoire consécutifs est assez rare. La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs sont systématiquement supportées, car dupliquer des ALU 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 et 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 possibels 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'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''. 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. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant. 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. ===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 4 instructions, compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément deux 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. 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. : 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 microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle. 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. ==L'évolution historique des processeurs superscalaires== L'évolution des CPU superscalaires historiques 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. 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. 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. Disons qu'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. Par exemple, 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. ===La double émission entière-flottante=== Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est d'émettre une micro-opération entière en même temps qu'une micro-opé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, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité. La plupart des premiers processeurs superscalaires étaient de ce type. Les exemples les plus notables sont les processeurs POWER 1 et ses dérivés comme le ''RISC Single Chip''. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle. [[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]] La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisations visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate]. ===L'émission parallèle des branchements=== Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur. 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|Emission multiple des opérations entières, implémentation courante]] Pour les branchements, deux solutions sont possibles. Avec la première, il y a émission parallèle des branchements, à savoir qu'il est possible d'émettre en branchement en parallèle de deux-trois instructions entières. Et cela impose la présence d'une unité de branchement séparée, avec son port d'émission. Avec la seconde, les branchements sont traités comme des instructions entières. Soit les branchements sont exécutés dans une ALU entière, soit il y a une unité de branchement reliée à un port d'émission pour les opérations entières. [[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Emission série et parallèle des branchements]] En général, les branchements sont exécutés comme des opérations entières. Cela permet d'économiser une unité de calcul et beaucoup de transistors dans l'unité d'émission/chargement/... {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 |- | * ALU entière * Multiplieur | * ALU entière * Barrel shifter * Unité de branchement | FPU |- | * Additions, soustractions, opérations bit à bit, etc. * Multiplication | * Additions, soustractions, opérations bit à bit, etc. * Décalages et rotations * Branchements |Opération flottante |} ===Les CPU superscalaires modernes=== Le premier CPU superscalaire grand public était le Pentium. Et c'était un CPU double émission qui n'implémentait pas la double émission entière-flottante, mais qui avait deux ALU entières. 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. Les CPU superscalaires modernes privilégient l'ajout d'ALU entières. Les multiplieurs n'ont pas été dupliqués, sauf sur les processeurs superscalaires très larges, car il est rare de devoir exécuter deux multiplications à la fois. Et c'est la même chose pour le ''barrel shifter''. La FPU aussi n'a pas été 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 donner quelques exemples, étudions les processeurs superscalaire PowerPC. Il y a eu plusieurs modèles, avec des jeux d'instructions légèrement différents, mais aussi et surtout des microarchitectures assez différentes. Le PowerPC 604 est lui passé à la quadruple émission, avec émission parallèle des branchements. En clair, c'était un CPU de type ''4-way + Branch''. 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, mais c'était le seul à être partagé. En clair, le CPU pouvait émettre deux INT + MEM + (soit MUL, soit FLOAT). Vint ensuite la génération suivante avec les PowerPC G3 et G4. Ils implémentaient la double émission + branchement. La différence avec le 603 est qu'ils intégraient deux ALU entières, pas une seule. Ils pouvaient donc exécuter deux opérations entières en même temps, la seule contrainte étant sur les multiplications (il n'y a qu'un seul multiplieur). Les combinaisons possibles étaient INT + INT , INT + MUL, INT + FLOAT, INT + MEM, FLOAT + MEM. ==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 simultanné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=== 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. Elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. La ''load/store queue'' reste une structure unique, mais on peut lui rajouter des ports. Et à ce peit jeu, il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire. [[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]] 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. Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre. Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Le Pentium 4 était dans ce cas, mais il était un peu seul dans sa catégorie. Un cas similaire est celui où l'unité mémoire est alimentée par une fenêtre de micro-opération mélangeant instructions mémoire et entières. Les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. Dans ce cas, l'émission multiple est rarement implémentée. Le cas suivant a une file pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique. Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui pouvaient seulement émettre une lecture en même temps qu'une écriture. Ils avaient deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse. [[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]] Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture demande d'utiliser une file d'écriture multiport, ce qui est assez compliqué. Implémenter des lectures en plus est plus simple : il suffit de rajouter une seconde file de µops LOAD et une unité de calcul, alimentées via un second port d'émission. Il est aussi possible de rajouter un second port de lecture au cache, mais ce n'est pas obligatoire. Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse. De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports de lecture et d'écriture car il y a plus de lecture que d'écritures. Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. : Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse. [[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]] Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettent plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail. ==Les optimisations des processeurs superscalaires larges== Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul. ===L'étape de chargement d'un CPU superscalaire large=== Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 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 autoaligné.]] Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements ! [[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]] Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez 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 microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total). Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois 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 microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement. Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations. ===La micro-fusion des processeurs Intel et AMD=== La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''. Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''. Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout. La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Le parallélisme mémoire | prevText=Le parallélisme mémoire | next=Exemples de microarchitectures CPU : le cas du x86 | nextText=Exemples de microarchitectures CPU : le cas du x86 }} </noinclude> bksv31slcvzwhnkdk43wyfppxnk96sq 772771 772767 2026-09-21T22:29:45Z Mewtow 31375 /* Les unités de calcul d'un processeur superscalaire */ 772771 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. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps. 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. 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. Deux branchements consécutifs sont quant à eux plus fréquents, mais exécuter deux branchements en même temps pose un problème : que faire quand les deux sont pris ? Deux accès mémoire consécutifs est assez rare. La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs sont systématiquement supportées, car dupliquer des ALU 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 et 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 possibels 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'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 les unités de calcul classique est assez simple, 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. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant. 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. ===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 4 instructions, compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément deux 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. 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. : 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 microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle. 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. ==L'évolution historique des processeurs superscalaires== L'évolution des CPU superscalaires historiques 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. 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. 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. Disons qu'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. Par exemple, 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. ===La double émission entière-flottante=== Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est d'émettre une micro-opération entière en même temps qu'une micro-opé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, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité. La plupart des premiers processeurs superscalaires étaient de ce type. Les exemples les plus notables sont les processeurs POWER 1 et ses dérivés comme le ''RISC Single Chip''. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle. [[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]] La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisations visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate]. ===L'émission parallèle des branchements=== Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur. 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|Emission multiple des opérations entières, implémentation courante]] Pour les branchements, deux solutions sont possibles. Avec la première, il y a émission parallèle des branchements, à savoir qu'il est possible d'émettre en branchement en parallèle de deux-trois instructions entières. Et cela impose la présence d'une unité de branchement séparée, avec son port d'émission. Avec la seconde, les branchements sont traités comme des instructions entières. Soit les branchements sont exécutés dans une ALU entière, soit il y a une unité de branchement reliée à un port d'émission pour les opérations entières. [[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Emission série et parallèle des branchements]] En général, les branchements sont exécutés comme des opérations entières. Cela permet d'économiser une unité de calcul et beaucoup de transistors dans l'unité d'émission/chargement/... {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 |- | * ALU entière * Multiplieur | * ALU entière * Barrel shifter * Unité de branchement | FPU |- | * Additions, soustractions, opérations bit à bit, etc. * Multiplication | * Additions, soustractions, opérations bit à bit, etc. * Décalages et rotations * Branchements |Opération flottante |} ===Les CPU superscalaires modernes=== Le premier CPU superscalaire grand public était le Pentium. Et c'était un CPU double émission qui n'implémentait pas la double émission entière-flottante, mais qui avait deux ALU entières. 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. Les CPU superscalaires modernes privilégient l'ajout d'ALU entières. Les multiplieurs n'ont pas été dupliqués, sauf sur les processeurs superscalaires très larges, car il est rare de devoir exécuter deux multiplications à la fois. Et c'est la même chose pour le ''barrel shifter''. La FPU aussi n'a pas été 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 donner quelques exemples, étudions les processeurs superscalaire PowerPC. Il y a eu plusieurs modèles, avec des jeux d'instructions légèrement différents, mais aussi et surtout des microarchitectures assez différentes. Le PowerPC 604 est lui passé à la quadruple émission, avec émission parallèle des branchements. En clair, c'était un CPU de type ''4-way + Branch''. 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, mais c'était le seul à être partagé. En clair, le CPU pouvait émettre deux INT + MEM + (soit MUL, soit FLOAT). Vint ensuite la génération suivante avec les PowerPC G3 et G4. Ils implémentaient la double émission + branchement. La différence avec le 603 est qu'ils intégraient deux ALU entières, pas une seule. Ils pouvaient donc exécuter deux opérations entières en même temps, la seule contrainte étant sur les multiplications (il n'y a qu'un seul multiplieur). Les combinaisons possibles étaient INT + INT , INT + MUL, INT + FLOAT, INT + MEM, FLOAT + MEM. ==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 simultanné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=== 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. Elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. La ''load/store queue'' reste une structure unique, mais on peut lui rajouter des ports. Et à ce peit jeu, il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire. [[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]] 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. Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre. Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Le Pentium 4 était dans ce cas, mais il était un peu seul dans sa catégorie. Un cas similaire est celui où l'unité mémoire est alimentée par une fenêtre de micro-opération mélangeant instructions mémoire et entières. Les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. Dans ce cas, l'émission multiple est rarement implémentée. Le cas suivant a une file pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique. Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui pouvaient seulement émettre une lecture en même temps qu'une écriture. Ils avaient deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse. [[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]] Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture demande d'utiliser une file d'écriture multiport, ce qui est assez compliqué. Implémenter des lectures en plus est plus simple : il suffit de rajouter une seconde file de µops LOAD et une unité de calcul, alimentées via un second port d'émission. Il est aussi possible de rajouter un second port de lecture au cache, mais ce n'est pas obligatoire. Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse. De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports de lecture et d'écriture car il y a plus de lecture que d'écritures. Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. : Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse. [[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]] Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettent plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail. ==Les optimisations des processeurs superscalaires larges== Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul. ===L'étape de chargement d'un CPU superscalaire large=== Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 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 autoaligné.]] Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements ! [[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]] Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez 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 microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total). Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois 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 microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement. Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations. ===La micro-fusion des processeurs Intel et AMD=== La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''. Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''. Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout. La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Le parallélisme mémoire | prevText=Le parallélisme mémoire | next=Exemples de microarchitectures CPU : le cas du x86 | nextText=Exemples de microarchitectures CPU : le cas du x86 }} </noinclude> cju3fyceobucqngogbcpua2sn4q9yro 772772 772771 2026-09-21T22:32:17Z Mewtow 31375 /* Les unités de calcul d'un processeur superscalaire */ 772772 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. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps. 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. 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. Deux branchements consécutifs sont quant à eux plus fréquents, mais exécuter deux branchements en même temps pose un problème : que faire quand les deux sont pris ? Deux accès mémoire consécutifs est assez rare. La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs sont systématiquement supportées, car dupliquer des ALU 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 et 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 possibels 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'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 les unités de calcul classique est assez simple, 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. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant. 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. ===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 4 instructions, compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément deux 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. 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. : 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 microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle. 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. ==L'évolution historique des processeurs superscalaires== L'évolution des CPU superscalaires historiques 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. 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. 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. Disons qu'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. Par exemple, 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. ===La double émission entière-flottante=== Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est d'émettre une micro-opération entière en même temps qu'une micro-opé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, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité. La plupart des premiers processeurs superscalaires étaient de ce type. Les exemples les plus notables sont les processeurs POWER 1 et ses dérivés comme le ''RISC Single Chip''. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle. [[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]] La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisations visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate]. ===L'émission parallèle des branchements=== Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur. 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|Emission multiple des opérations entières, implémentation courante]] Pour les branchements, deux solutions sont possibles. Avec la première, il y a émission parallèle des branchements, à savoir qu'il est possible d'émettre en branchement en parallèle de deux-trois instructions entières. Et cela impose la présence d'une unité de branchement séparée, avec son port d'émission. Avec la seconde, les branchements sont traités comme des instructions entières. Soit les branchements sont exécutés dans une ALU entière, soit il y a une unité de branchement reliée à un port d'émission pour les opérations entières. [[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Emission série et parallèle des branchements]] En général, les branchements sont exécutés comme des opérations entières. Cela permet d'économiser une unité de calcul et beaucoup de transistors dans l'unité d'émission/chargement/... {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 |- | * ALU entière * Multiplieur | * ALU entière * Barrel shifter * Unité de branchement | FPU |- | * Additions, soustractions, opérations bit à bit, etc. * Multiplication | * Additions, soustractions, opérations bit à bit, etc. * Décalages et rotations * Branchements |Opération flottante |} ===Les CPU superscalaires modernes=== Le premier CPU superscalaire grand public était le Pentium. Et c'était un CPU double émission qui n'implémentait pas la double émission entière-flottante, mais qui avait deux ALU entières. 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. Les CPU superscalaires modernes privilégient l'ajout d'ALU entières. Les multiplieurs n'ont pas été dupliqués, sauf sur les processeurs superscalaires très larges, car il est rare de devoir exécuter deux multiplications à la fois. Et c'est la même chose pour le ''barrel shifter''. La FPU aussi n'a pas été 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 donner quelques exemples, étudions les processeurs superscalaire PowerPC. Il y a eu plusieurs modèles, avec des jeux d'instructions légèrement différents, mais aussi et surtout des microarchitectures assez différentes. Le PowerPC 604 est lui passé à la quadruple émission, avec émission parallèle des branchements. En clair, c'était un CPU de type ''4-way + Branch''. 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, mais c'était le seul à être partagé. En clair, le CPU pouvait émettre deux INT + MEM + (soit MUL, soit FLOAT). Vint ensuite la génération suivante avec les PowerPC G3 et G4. Ils implémentaient la double émission + branchement. La différence avec le 603 est qu'ils intégraient deux ALU entières, pas une seule. Ils pouvaient donc exécuter deux opérations entières en même temps, la seule contrainte étant sur les multiplications (il n'y a qu'un seul multiplieur). Les combinaisons possibles étaient INT + INT , INT + MUL, INT + FLOAT, INT + MEM, FLOAT + MEM. ==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 simultanné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=== 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. Elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. La ''load/store queue'' reste une structure unique, mais on peut lui rajouter des ports. Et à ce peit jeu, il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire. [[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]] 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. Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre. Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Le Pentium 4 était dans ce cas, mais il était un peu seul dans sa catégorie. Un cas similaire est celui où l'unité mémoire est alimentée par une fenêtre de micro-opération mélangeant instructions mémoire et entières. Les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. Dans ce cas, l'émission multiple est rarement implémentée. Le cas suivant a une file pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique. Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui pouvaient seulement émettre une lecture en même temps qu'une écriture. Ils avaient deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse. [[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]] Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture demande d'utiliser une file d'écriture multiport, ce qui est assez compliqué. Implémenter des lectures en plus est plus simple : il suffit de rajouter une seconde file de µops LOAD et une unité de calcul, alimentées via un second port d'émission. Il est aussi possible de rajouter un second port de lecture au cache, mais ce n'est pas obligatoire. Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse. De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports de lecture et d'écriture car il y a plus de lecture que d'écritures. Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. : Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse. [[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]] Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettent plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail. ==Les optimisations des processeurs superscalaires larges== Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul. ===L'étape de chargement d'un CPU superscalaire large=== Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 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 autoaligné.]] Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements ! [[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]] Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez 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 microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total). Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois 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 microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement. Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations. ===La micro-fusion des processeurs Intel et AMD=== La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''. Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''. Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout. La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Le parallélisme mémoire | prevText=Le parallélisme mémoire | next=Exemples de microarchitectures CPU : le cas du x86 | nextText=Exemples de microarchitectures CPU : le cas du x86 }} </noinclude> kh5sjlfdikf8th4pvx67gnfdksdz11y 772773 772772 2026-09-21T22:32:43Z Mewtow 31375 /* Les unités de calcul d'un processeur superscalaire */ 772773 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. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps. 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. 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. Deux branchements consécutifs sont quant à eux plus fréquents, mais exécuter deux branchements en même temps pose un problème : que faire quand les deux sont pris ? Deux accès mémoire consécutifs est assez rare. La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs sont systématiquement supportées, car dupliquer des ALU 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 et 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 possibels 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'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, 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. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant. 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. ===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 4 instructions, compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément deux 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. 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. : 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 microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle. 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. ==L'évolution historique des processeurs superscalaires== L'évolution des CPU superscalaires historiques 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. 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. 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. Disons qu'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. Par exemple, 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. ===La double émission entière-flottante=== Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est d'émettre une micro-opération entière en même temps qu'une micro-opé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, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité. La plupart des premiers processeurs superscalaires étaient de ce type. Les exemples les plus notables sont les processeurs POWER 1 et ses dérivés comme le ''RISC Single Chip''. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle. [[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]] La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisations visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate]. ===L'émission parallèle des branchements=== Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur. 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|Emission multiple des opérations entières, implémentation courante]] Pour les branchements, deux solutions sont possibles. Avec la première, il y a émission parallèle des branchements, à savoir qu'il est possible d'émettre en branchement en parallèle de deux-trois instructions entières. Et cela impose la présence d'une unité de branchement séparée, avec son port d'émission. Avec la seconde, les branchements sont traités comme des instructions entières. Soit les branchements sont exécutés dans une ALU entière, soit il y a une unité de branchement reliée à un port d'émission pour les opérations entières. [[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Emission série et parallèle des branchements]] En général, les branchements sont exécutés comme des opérations entières. Cela permet d'économiser une unité de calcul et beaucoup de transistors dans l'unité d'émission/chargement/... {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 |- | * ALU entière * Multiplieur | * ALU entière * Barrel shifter * Unité de branchement | FPU |- | * Additions, soustractions, opérations bit à bit, etc. * Multiplication | * Additions, soustractions, opérations bit à bit, etc. * Décalages et rotations * Branchements |Opération flottante |} ===Les CPU superscalaires modernes=== Le premier CPU superscalaire grand public était le Pentium. Et c'était un CPU double émission qui n'implémentait pas la double émission entière-flottante, mais qui avait deux ALU entières. 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. Les CPU superscalaires modernes privilégient l'ajout d'ALU entières. Les multiplieurs n'ont pas été dupliqués, sauf sur les processeurs superscalaires très larges, car il est rare de devoir exécuter deux multiplications à la fois. Et c'est la même chose pour le ''barrel shifter''. La FPU aussi n'a pas été 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 donner quelques exemples, étudions les processeurs superscalaire PowerPC. Il y a eu plusieurs modèles, avec des jeux d'instructions légèrement différents, mais aussi et surtout des microarchitectures assez différentes. Le PowerPC 604 est lui passé à la quadruple émission, avec émission parallèle des branchements. En clair, c'était un CPU de type ''4-way + Branch''. 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, mais c'était le seul à être partagé. En clair, le CPU pouvait émettre deux INT + MEM + (soit MUL, soit FLOAT). Vint ensuite la génération suivante avec les PowerPC G3 et G4. Ils implémentaient la double émission + branchement. La différence avec le 603 est qu'ils intégraient deux ALU entières, pas une seule. Ils pouvaient donc exécuter deux opérations entières en même temps, la seule contrainte étant sur les multiplications (il n'y a qu'un seul multiplieur). Les combinaisons possibles étaient INT + INT , INT + MUL, INT + FLOAT, INT + MEM, FLOAT + MEM. ==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 simultanné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=== 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. Elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. La ''load/store queue'' reste une structure unique, mais on peut lui rajouter des ports. Et à ce peit jeu, il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire. [[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]] 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. Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre. Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Le Pentium 4 était dans ce cas, mais il était un peu seul dans sa catégorie. Un cas similaire est celui où l'unité mémoire est alimentée par une fenêtre de micro-opération mélangeant instructions mémoire et entières. Les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. Dans ce cas, l'émission multiple est rarement implémentée. Le cas suivant a une file pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique. Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui pouvaient seulement émettre une lecture en même temps qu'une écriture. Ils avaient deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse. [[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]] Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture demande d'utiliser une file d'écriture multiport, ce qui est assez compliqué. Implémenter des lectures en plus est plus simple : il suffit de rajouter une seconde file de µops LOAD et une unité de calcul, alimentées via un second port d'émission. Il est aussi possible de rajouter un second port de lecture au cache, mais ce n'est pas obligatoire. Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse. De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports de lecture et d'écriture car il y a plus de lecture que d'écritures. Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. : Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse. [[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]] Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettent plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail. ==Les optimisations des processeurs superscalaires larges== Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul. ===L'étape de chargement d'un CPU superscalaire large=== Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 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 autoaligné.]] Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements ! [[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]] Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez 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 microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total). Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois 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 microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement. Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations. ===La micro-fusion des processeurs Intel et AMD=== La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''. Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''. Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout. La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Le parallélisme mémoire | prevText=Le parallélisme mémoire | next=Exemples de microarchitectures CPU : le cas du x86 | nextText=Exemples de microarchitectures CPU : le cas du x86 }} </noinclude> 7qj0xm5e5soi2m347ugsvs31ll44nuz 772775 772773 2026-09-21T22:47:07Z Mewtow 31375 /* Les CPU superscalaires modernes */ 772775 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. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps. 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. 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. Deux branchements consécutifs sont quant à eux plus fréquents, mais exécuter deux branchements en même temps pose un problème : que faire quand les deux sont pris ? Deux accès mémoire consécutifs est assez rare. La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs sont systématiquement supportées, car dupliquer des ALU 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 et 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 possibels 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'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, 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. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant. 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. ===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 4 instructions, compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément deux 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. 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. : 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 microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle. 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. ==L'évolution historique des processeurs superscalaires== L'évolution des CPU superscalaires historiques 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. 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. 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. Disons qu'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. Par exemple, 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. ===La double émission entière-flottante=== Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est d'émettre une micro-opération entière en même temps qu'une micro-opé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, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité. La plupart des premiers processeurs superscalaires étaient de ce type. Les exemples les plus notables sont les processeurs POWER 1 et ses dérivés comme le ''RISC Single Chip''. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle. [[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]] La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisations visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate]. ===L'émission parallèle des branchements=== Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur. 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|Emission multiple des opérations entières, implémentation courante]] Pour les branchements, deux solutions sont possibles. Avec la première, il y a émission parallèle des branchements, à savoir qu'il est possible d'émettre en branchement en parallèle de deux-trois instructions entières. Et cela impose la présence d'une unité de branchement séparée, avec son port d'émission. Avec la seconde, les branchements sont traités comme des instructions entières. Soit les branchements sont exécutés dans une ALU entière, soit il y a une unité de branchement reliée à un port d'émission pour les opérations entières. [[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Emission série et parallèle des branchements]] En général, les branchements sont exécutés comme des opérations entières. Cela permet d'économiser une unité de calcul et beaucoup de transistors dans l'unité d'émission/chargement/... {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 |- | * ALU entière * Multiplieur | * ALU entière * Barrel shifter * Unité de branchement | FPU |- | * Additions, soustractions, opérations bit à bit, etc. * Multiplication | * Additions, soustractions, opérations bit à bit, etc. * Décalages et rotations * Branchements |Opération flottante |} ===Les CPU superscalaires modernes=== Le premier CPU superscalaire grand public était le Pentium. Et c'était un CPU double émission qui n'implémentait pas la double émission entière-flottante, mais qui avait deux ALU entières. 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. Les CPU superscalaires modernes privilégient l'ajout d'ALU entières. Les multiplieurs n'ont pas été dupliqués, sauf sur les processeurs superscalaires très larges, car il est rare de devoir exécuter deux multiplications à la fois. Et c'est la même chose pour le ''barrel shifter''. La FPU aussi n'a pas été 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. ==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 simultanné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=== 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. Elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. La ''load/store queue'' reste une structure unique, mais on peut lui rajouter des ports. Et à ce peit jeu, il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire. [[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]] 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. Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre. Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Le Pentium 4 était dans ce cas, mais il était un peu seul dans sa catégorie. Un cas similaire est celui où l'unité mémoire est alimentée par une fenêtre de micro-opération mélangeant instructions mémoire et entières. Les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. Dans ce cas, l'émission multiple est rarement implémentée. Le cas suivant a une file pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique. Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui pouvaient seulement émettre une lecture en même temps qu'une écriture. Ils avaient deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse. [[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]] Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture demande d'utiliser une file d'écriture multiport, ce qui est assez compliqué. Implémenter des lectures en plus est plus simple : il suffit de rajouter une seconde file de µops LOAD et une unité de calcul, alimentées via un second port d'émission. Il est aussi possible de rajouter un second port de lecture au cache, mais ce n'est pas obligatoire. Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse. De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports de lecture et d'écriture car il y a plus de lecture que d'écritures. Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. : Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse. [[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]] Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettent plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail. ==Les optimisations des processeurs superscalaires larges== Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul. ===L'étape de chargement d'un CPU superscalaire large=== Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 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 autoaligné.]] Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements ! [[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]] Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez 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 microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total). Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois 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 microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement. Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations. ===La micro-fusion des processeurs Intel et AMD=== La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''. Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''. Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout. La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Le parallélisme mémoire | prevText=Le parallélisme mémoire | next=Exemples de microarchitectures CPU : le cas du x86 | nextText=Exemples de microarchitectures CPU : le cas du x86 }} </noinclude> lbf4whswqft36cxob7llfdstcommwfu 772776 772775 2026-09-21T22:49:56Z Mewtow 31375 /* L'unité d'émission d'un processeur superscalaire */ 772776 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. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps. 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. 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. Deux branchements consécutifs sont quant à eux plus fréquents, mais exécuter deux branchements en même temps pose un problème : que faire quand les deux sont pris ? Deux accès mémoire consécutifs est assez rare. La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs sont systématiquement supportées, car dupliquer des ALU 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 et 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 possibels 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'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, 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. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant. 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. ===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 4 instructions, compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément deux 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. Le mieux est d'illsutrer le tout avec des exemples concrets. Le PowerPC 604 est lui passé à la quadruple émission, avec émission parallèle des branchements. En clair, c'était un CPU de type ''4-way + Branch''. 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, mais c'était le seul à être partagé. En clair, le CPU pouvait émettre deux INT + MEM + (soit MUL, soit FLOAT). 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 + FLOAT, INT + 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. : 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 microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle. 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. ==L'évolution historique des processeurs superscalaires== L'évolution des CPU superscalaires historiques 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. 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. 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. Disons qu'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. Par exemple, 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. ===La double émission entière-flottante=== Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est d'émettre une micro-opération entière en même temps qu'une micro-opé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, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité. La plupart des premiers processeurs superscalaires étaient de ce type. Les exemples les plus notables sont les processeurs POWER 1 et ses dérivés comme le ''RISC Single Chip''. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle. [[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]] La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisations visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate]. ===L'émission parallèle des branchements=== Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur. 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|Emission multiple des opérations entières, implémentation courante]] Pour les branchements, deux solutions sont possibles. Avec la première, il y a émission parallèle des branchements, à savoir qu'il est possible d'émettre en branchement en parallèle de deux-trois instructions entières. Et cela impose la présence d'une unité de branchement séparée, avec son port d'émission. Avec la seconde, les branchements sont traités comme des instructions entières. Soit les branchements sont exécutés dans une ALU entière, soit il y a une unité de branchement reliée à un port d'émission pour les opérations entières. [[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Emission série et parallèle des branchements]] En général, les branchements sont exécutés comme des opérations entières. Cela permet d'économiser une unité de calcul et beaucoup de transistors dans l'unité d'émission/chargement/... {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 |- | * ALU entière * Multiplieur | * ALU entière * Barrel shifter * Unité de branchement | FPU |- | * Additions, soustractions, opérations bit à bit, etc. * Multiplication | * Additions, soustractions, opérations bit à bit, etc. * Décalages et rotations * Branchements |Opération flottante |} ===Les CPU superscalaires modernes=== Le premier CPU superscalaire grand public était le Pentium. Et c'était un CPU double émission qui n'implémentait pas la double émission entière-flottante, mais qui avait deux ALU entières. 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. Les CPU superscalaires modernes privilégient l'ajout d'ALU entières. Les multiplieurs n'ont pas été dupliqués, sauf sur les processeurs superscalaires très larges, car il est rare de devoir exécuter deux multiplications à la fois. Et c'est la même chose pour le ''barrel shifter''. La FPU aussi n'a pas été 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. ==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 simultanné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=== 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. Elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. La ''load/store queue'' reste une structure unique, mais on peut lui rajouter des ports. Et à ce peit jeu, il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire. [[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]] 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. Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre. Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Le Pentium 4 était dans ce cas, mais il était un peu seul dans sa catégorie. Un cas similaire est celui où l'unité mémoire est alimentée par une fenêtre de micro-opération mélangeant instructions mémoire et entières. Les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. Dans ce cas, l'émission multiple est rarement implémentée. Le cas suivant a une file pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique. Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui pouvaient seulement émettre une lecture en même temps qu'une écriture. Ils avaient deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse. [[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]] Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture demande d'utiliser une file d'écriture multiport, ce qui est assez compliqué. Implémenter des lectures en plus est plus simple : il suffit de rajouter une seconde file de µops LOAD et une unité de calcul, alimentées via un second port d'émission. Il est aussi possible de rajouter un second port de lecture au cache, mais ce n'est pas obligatoire. Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse. De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports de lecture et d'écriture car il y a plus de lecture que d'écritures. Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. : Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse. [[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]] Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettent plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail. ==Les optimisations des processeurs superscalaires larges== Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul. ===L'étape de chargement d'un CPU superscalaire large=== Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 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 autoaligné.]] Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements ! [[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]] Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez 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 microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total). Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois 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 microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement. Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations. ===La micro-fusion des processeurs Intel et AMD=== La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''. Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''. Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout. La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Le parallélisme mémoire | prevText=Le parallélisme mémoire | next=Exemples de microarchitectures CPU : le cas du x86 | nextText=Exemples de microarchitectures CPU : le cas du x86 }} </noinclude> azt8maeva1h3h4ul8pqhobvlbxa5hjg 772777 772776 2026-09-21T22:51:21Z Mewtow 31375 /* L'unité d'émission d'un processeur superscalaire */ 772777 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. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps. 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. 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. Deux branchements consécutifs sont quant à eux plus fréquents, mais exécuter deux branchements en même temps pose un problème : que faire quand les deux sont pris ? Deux accès mémoire consécutifs est assez rare. La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs sont systématiquement supportées, car dupliquer des ALU 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 et 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 possibels 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'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, 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. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant. 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. ===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 4 instructions, compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément deux 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 + FLOAT, INT + 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 microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle. 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. ==L'évolution historique des processeurs superscalaires== L'évolution des CPU superscalaires historiques 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. 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. 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. Disons qu'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. Par exemple, 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. ===La double émission entière-flottante=== Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est d'émettre une micro-opération entière en même temps qu'une micro-opé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, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité. La plupart des premiers processeurs superscalaires étaient de ce type. Les exemples les plus notables sont les processeurs POWER 1 et ses dérivés comme le ''RISC Single Chip''. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle. [[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]] La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisations visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate]. ===L'émission parallèle des branchements=== Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur. 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|Emission multiple des opérations entières, implémentation courante]] Pour les branchements, deux solutions sont possibles. Avec la première, il y a émission parallèle des branchements, à savoir qu'il est possible d'émettre en branchement en parallèle de deux-trois instructions entières. Et cela impose la présence d'une unité de branchement séparée, avec son port d'émission. Avec la seconde, les branchements sont traités comme des instructions entières. Soit les branchements sont exécutés dans une ALU entière, soit il y a une unité de branchement reliée à un port d'émission pour les opérations entières. [[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Emission série et parallèle des branchements]] En général, les branchements sont exécutés comme des opérations entières. Cela permet d'économiser une unité de calcul et beaucoup de transistors dans l'unité d'émission/chargement/... {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 |- | * ALU entière * Multiplieur | * ALU entière * Barrel shifter * Unité de branchement | FPU |- | * Additions, soustractions, opérations bit à bit, etc. * Multiplication | * Additions, soustractions, opérations bit à bit, etc. * Décalages et rotations * Branchements |Opération flottante |} ===Les CPU superscalaires modernes=== Le premier CPU superscalaire grand public était le Pentium. Et c'était un CPU double émission qui n'implémentait pas la double émission entière-flottante, mais qui avait deux ALU entières. 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. Les CPU superscalaires modernes privilégient l'ajout d'ALU entières. Les multiplieurs n'ont pas été dupliqués, sauf sur les processeurs superscalaires très larges, car il est rare de devoir exécuter deux multiplications à la fois. Et c'est la même chose pour le ''barrel shifter''. La FPU aussi n'a pas été 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. ==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 simultanné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=== 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. Elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. La ''load/store queue'' reste une structure unique, mais on peut lui rajouter des ports. Et à ce peit jeu, il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire. [[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]] 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. Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre. Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Le Pentium 4 était dans ce cas, mais il était un peu seul dans sa catégorie. Un cas similaire est celui où l'unité mémoire est alimentée par une fenêtre de micro-opération mélangeant instructions mémoire et entières. Les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. Dans ce cas, l'émission multiple est rarement implémentée. Le cas suivant a une file pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique. Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui pouvaient seulement émettre une lecture en même temps qu'une écriture. Ils avaient deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse. [[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]] Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture demande d'utiliser une file d'écriture multiport, ce qui est assez compliqué. Implémenter des lectures en plus est plus simple : il suffit de rajouter une seconde file de µops LOAD et une unité de calcul, alimentées via un second port d'émission. Il est aussi possible de rajouter un second port de lecture au cache, mais ce n'est pas obligatoire. Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse. De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports de lecture et d'écriture car il y a plus de lecture que d'écritures. Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. : Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse. [[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]] Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettent plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail. ==Les optimisations des processeurs superscalaires larges== Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul. ===L'étape de chargement d'un CPU superscalaire large=== Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 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 autoaligné.]] Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements ! [[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]] Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez 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 microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total). Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois 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 microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement. Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations. ===La micro-fusion des processeurs Intel et AMD=== La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''. Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''. Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout. La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Le parallélisme mémoire | prevText=Le parallélisme mémoire | next=Exemples de microarchitectures CPU : le cas du x86 | nextText=Exemples de microarchitectures CPU : le cas du x86 }} </noinclude> qvrn1bm3paicdbzvt2a1dvsovkecsb1 772778 772777 2026-09-21T22:52:16Z Mewtow 31375 /* L'unité d'émission d'un processeur superscalaire */ 772778 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. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps. 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. 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. Deux branchements consécutifs sont quant à eux plus fréquents, mais exécuter deux branchements en même temps pose un problème : que faire quand les deux sont pris ? Deux accès mémoire consécutifs est assez rare. La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs sont systématiquement supportées, car dupliquer des ALU 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 et 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 possibels 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'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, 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. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant. 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. ===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 + FLOAT, INT + 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 microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle. 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. ==L'évolution historique des processeurs superscalaires== L'évolution des CPU superscalaires historiques 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. 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. 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. Disons qu'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. Par exemple, 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. ===La double émission entière-flottante=== Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est d'émettre une micro-opération entière en même temps qu'une micro-opé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, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité. La plupart des premiers processeurs superscalaires étaient de ce type. Les exemples les plus notables sont les processeurs POWER 1 et ses dérivés comme le ''RISC Single Chip''. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle. [[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]] La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisations visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate]. ===L'émission parallèle des branchements=== Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur. 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|Emission multiple des opérations entières, implémentation courante]] Pour les branchements, deux solutions sont possibles. Avec la première, il y a émission parallèle des branchements, à savoir qu'il est possible d'émettre en branchement en parallèle de deux-trois instructions entières. Et cela impose la présence d'une unité de branchement séparée, avec son port d'émission. Avec la seconde, les branchements sont traités comme des instructions entières. Soit les branchements sont exécutés dans une ALU entière, soit il y a une unité de branchement reliée à un port d'émission pour les opérations entières. [[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Emission série et parallèle des branchements]] En général, les branchements sont exécutés comme des opérations entières. Cela permet d'économiser une unité de calcul et beaucoup de transistors dans l'unité d'émission/chargement/... {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 |- | * ALU entière * Multiplieur | * ALU entière * Barrel shifter * Unité de branchement | FPU |- | * Additions, soustractions, opérations bit à bit, etc. * Multiplication | * Additions, soustractions, opérations bit à bit, etc. * Décalages et rotations * Branchements |Opération flottante |} ===Les CPU superscalaires modernes=== Le premier CPU superscalaire grand public était le Pentium. Et c'était un CPU double émission qui n'implémentait pas la double émission entière-flottante, mais qui avait deux ALU entières. 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. Les CPU superscalaires modernes privilégient l'ajout d'ALU entières. Les multiplieurs n'ont pas été dupliqués, sauf sur les processeurs superscalaires très larges, car il est rare de devoir exécuter deux multiplications à la fois. Et c'est la même chose pour le ''barrel shifter''. La FPU aussi n'a pas été 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. ==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 simultanné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=== 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. Elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. La ''load/store queue'' reste une structure unique, mais on peut lui rajouter des ports. Et à ce peit jeu, il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire. [[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]] 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. Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre. Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Le Pentium 4 était dans ce cas, mais il était un peu seul dans sa catégorie. Un cas similaire est celui où l'unité mémoire est alimentée par une fenêtre de micro-opération mélangeant instructions mémoire et entières. Les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. Dans ce cas, l'émission multiple est rarement implémentée. Le cas suivant a une file pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique. Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui pouvaient seulement émettre une lecture en même temps qu'une écriture. Ils avaient deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse. [[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]] Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture demande d'utiliser une file d'écriture multiport, ce qui est assez compliqué. Implémenter des lectures en plus est plus simple : il suffit de rajouter une seconde file de µops LOAD et une unité de calcul, alimentées via un second port d'émission. Il est aussi possible de rajouter un second port de lecture au cache, mais ce n'est pas obligatoire. Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse. De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports de lecture et d'écriture car il y a plus de lecture que d'écritures. Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. : Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse. [[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]] Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettent plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail. ==Les optimisations des processeurs superscalaires larges== Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul. ===L'étape de chargement d'un CPU superscalaire large=== Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 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 autoaligné.]] Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements ! [[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]] Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez 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 microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total). Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois 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 microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement. Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations. ===La micro-fusion des processeurs Intel et AMD=== La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''. Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''. Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout. La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Le parallélisme mémoire | prevText=Le parallélisme mémoire | next=Exemples de microarchitectures CPU : le cas du x86 | nextText=Exemples de microarchitectures CPU : le cas du x86 }} </noinclude> dnmdpv8lgxwkwzpwoz2nt0gh7bgdd6u 772779 772778 2026-09-21T22:52:49Z Mewtow 31375 /* L'unité d'émission d'un processeur superscalaire */ 772779 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. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps. 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. 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. Deux branchements consécutifs sont quant à eux plus fréquents, mais exécuter deux branchements en même temps pose un problème : que faire quand les deux sont pris ? Deux accès mémoire consécutifs est assez rare. La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs sont systématiquement supportées, car dupliquer des ALU 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 et 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 possibels 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'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, 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. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant. 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. ===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 microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle. 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. ==L'évolution historique des processeurs superscalaires== L'évolution des CPU superscalaires historiques 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. 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. 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. Disons qu'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. Par exemple, 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. ===La double émission entière-flottante=== Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est d'émettre une micro-opération entière en même temps qu'une micro-opé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, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité. La plupart des premiers processeurs superscalaires étaient de ce type. Les exemples les plus notables sont les processeurs POWER 1 et ses dérivés comme le ''RISC Single Chip''. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle. [[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]] La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisations visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate]. ===L'émission parallèle des branchements=== Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur. 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|Emission multiple des opérations entières, implémentation courante]] Pour les branchements, deux solutions sont possibles. Avec la première, il y a émission parallèle des branchements, à savoir qu'il est possible d'émettre en branchement en parallèle de deux-trois instructions entières. Et cela impose la présence d'une unité de branchement séparée, avec son port d'émission. Avec la seconde, les branchements sont traités comme des instructions entières. Soit les branchements sont exécutés dans une ALU entière, soit il y a une unité de branchement reliée à un port d'émission pour les opérations entières. [[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Emission série et parallèle des branchements]] En général, les branchements sont exécutés comme des opérations entières. Cela permet d'économiser une unité de calcul et beaucoup de transistors dans l'unité d'émission/chargement/... {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 |- | * ALU entière * Multiplieur | * ALU entière * Barrel shifter * Unité de branchement | FPU |- | * Additions, soustractions, opérations bit à bit, etc. * Multiplication | * Additions, soustractions, opérations bit à bit, etc. * Décalages et rotations * Branchements |Opération flottante |} ===Les CPU superscalaires modernes=== Le premier CPU superscalaire grand public était le Pentium. Et c'était un CPU double émission qui n'implémentait pas la double émission entière-flottante, mais qui avait deux ALU entières. 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. Les CPU superscalaires modernes privilégient l'ajout d'ALU entières. Les multiplieurs n'ont pas été dupliqués, sauf sur les processeurs superscalaires très larges, car il est rare de devoir exécuter deux multiplications à la fois. Et c'est la même chose pour le ''barrel shifter''. La FPU aussi n'a pas été 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. ==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 simultanné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=== 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. Elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. La ''load/store queue'' reste une structure unique, mais on peut lui rajouter des ports. Et à ce peit jeu, il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire. [[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]] 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. Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre. Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Le Pentium 4 était dans ce cas, mais il était un peu seul dans sa catégorie. Un cas similaire est celui où l'unité mémoire est alimentée par une fenêtre de micro-opération mélangeant instructions mémoire et entières. Les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. Dans ce cas, l'émission multiple est rarement implémentée. Le cas suivant a une file pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique. Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui pouvaient seulement émettre une lecture en même temps qu'une écriture. Ils avaient deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse. [[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]] Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture demande d'utiliser une file d'écriture multiport, ce qui est assez compliqué. Implémenter des lectures en plus est plus simple : il suffit de rajouter une seconde file de µops LOAD et une unité de calcul, alimentées via un second port d'émission. Il est aussi possible de rajouter un second port de lecture au cache, mais ce n'est pas obligatoire. Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse. De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports de lecture et d'écriture car il y a plus de lecture que d'écritures. Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. : Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse. [[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]] Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettent plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail. ==Les optimisations des processeurs superscalaires larges== Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul. ===L'étape de chargement d'un CPU superscalaire large=== Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 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 autoaligné.]] Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements ! [[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]] Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez 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 microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total). Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois 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 microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement. Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations. ===La micro-fusion des processeurs Intel et AMD=== La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''. Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''. Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout. La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Le parallélisme mémoire | prevText=Le parallélisme mémoire | next=Exemples de microarchitectures CPU : le cas du x86 | nextText=Exemples de microarchitectures CPU : le cas du x86 }} </noinclude> 5jjte19fv26u3piim91i4ae9k378hmg 772780 772779 2026-09-21T22:54:23Z Mewtow 31375 /* La double émission entière-flottante */ 772780 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. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps. 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. 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. Deux branchements consécutifs sont quant à eux plus fréquents, mais exécuter deux branchements en même temps pose un problème : que faire quand les deux sont pris ? Deux accès mémoire consécutifs est assez rare. La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs sont systématiquement supportées, car dupliquer des ALU 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 et 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 possibels 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'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, 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. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant. 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. ===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 microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle. 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. ==L'évolution historique des processeurs superscalaires== L'évolution des CPU superscalaires historiques 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. 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. 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. Disons qu'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. Par exemple, 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. ===La double émission entière-flottante=== Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est d'émettre une micro-opération entière en même temps qu'une micro-opé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]] La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisations visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate]. ===L'émission parallèle des branchements=== Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur. 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|Emission multiple des opérations entières, implémentation courante]] Pour les branchements, deux solutions sont possibles. Avec la première, il y a émission parallèle des branchements, à savoir qu'il est possible d'émettre en branchement en parallèle de deux-trois instructions entières. Et cela impose la présence d'une unité de branchement séparée, avec son port d'émission. Avec la seconde, les branchements sont traités comme des instructions entières. Soit les branchements sont exécutés dans une ALU entière, soit il y a une unité de branchement reliée à un port d'émission pour les opérations entières. [[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Emission série et parallèle des branchements]] En général, les branchements sont exécutés comme des opérations entières. Cela permet d'économiser une unité de calcul et beaucoup de transistors dans l'unité d'émission/chargement/... {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 |- | * ALU entière * Multiplieur | * ALU entière * Barrel shifter * Unité de branchement | FPU |- | * Additions, soustractions, opérations bit à bit, etc. * Multiplication | * Additions, soustractions, opérations bit à bit, etc. * Décalages et rotations * Branchements |Opération flottante |} ===Les CPU superscalaires modernes=== Le premier CPU superscalaire grand public était le Pentium. Et c'était un CPU double émission qui n'implémentait pas la double émission entière-flottante, mais qui avait deux ALU entières. 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. Les CPU superscalaires modernes privilégient l'ajout d'ALU entières. Les multiplieurs n'ont pas été dupliqués, sauf sur les processeurs superscalaires très larges, car il est rare de devoir exécuter deux multiplications à la fois. Et c'est la même chose pour le ''barrel shifter''. La FPU aussi n'a pas été 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. ==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 simultanné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=== 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. Elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. La ''load/store queue'' reste une structure unique, mais on peut lui rajouter des ports. Et à ce peit jeu, il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire. [[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]] 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. Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre. Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Le Pentium 4 était dans ce cas, mais il était un peu seul dans sa catégorie. Un cas similaire est celui où l'unité mémoire est alimentée par une fenêtre de micro-opération mélangeant instructions mémoire et entières. Les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. Dans ce cas, l'émission multiple est rarement implémentée. Le cas suivant a une file pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique. Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui pouvaient seulement émettre une lecture en même temps qu'une écriture. Ils avaient deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse. [[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]] Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture demande d'utiliser une file d'écriture multiport, ce qui est assez compliqué. Implémenter des lectures en plus est plus simple : il suffit de rajouter une seconde file de µops LOAD et une unité de calcul, alimentées via un second port d'émission. Il est aussi possible de rajouter un second port de lecture au cache, mais ce n'est pas obligatoire. Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse. De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports de lecture et d'écriture car il y a plus de lecture que d'écritures. Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. : Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse. [[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]] Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettent plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail. ==Les optimisations des processeurs superscalaires larges== Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul. ===L'étape de chargement d'un CPU superscalaire large=== Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 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 autoaligné.]] Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements ! [[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]] Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez 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 microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total). Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois 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 microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement. Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations. ===La micro-fusion des processeurs Intel et AMD=== La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''. Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''. Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout. La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Le parallélisme mémoire | prevText=Le parallélisme mémoire | next=Exemples de microarchitectures CPU : le cas du x86 | nextText=Exemples de microarchitectures CPU : le cas du x86 }} </noinclude> avnoubrf6tpvpuus2y5f4dvebq16r0o 772781 772780 2026-09-21T22:57:07Z Mewtow 31375 /* La double émission entière-flottante */ 772781 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. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps. 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. 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. Deux branchements consécutifs sont quant à eux plus fréquents, mais exécuter deux branchements en même temps pose un problème : que faire quand les deux sont pris ? Deux accès mémoire consécutifs est assez rare. La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs sont systématiquement supportées, car dupliquer des ALU 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 et 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 possibels 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'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, 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. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant. 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. ===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 microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle. 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. ==L'évolution historique des processeurs superscalaires== L'évolution des CPU superscalaires historiques 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. 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. 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. Disons qu'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. Par exemple, 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. ===La double émission entière-flottante=== Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est d'émettre une micro-opération entière en même temps qu'une micro-opé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]] La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisations visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate]. Il faut préciser que les µuops 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'atterir 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|Emission multiple des opérations entières, implémentation courante]] Pour les branchements, deux solutions sont possibles. Avec la première, il y a émission parallèle des branchements, à savoir qu'il est possible d'émettre en branchement en parallèle de deux-trois instructions entières. Et cela impose la présence d'une unité de branchement séparée, avec son port d'émission. Avec la seconde, les branchements sont traités comme des instructions entières. Soit les branchements sont exécutés dans une ALU entière, soit il y a une unité de branchement reliée à un port d'émission pour les opérations entières. [[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Emission série et parallèle des branchements]] En général, les branchements sont exécutés comme des opérations entières. Cela permet d'économiser une unité de calcul et beaucoup de transistors dans l'unité d'émission/chargement/... {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 |- | * ALU entière * Multiplieur | * ALU entière * Barrel shifter * Unité de branchement | FPU |- | * Additions, soustractions, opérations bit à bit, etc. * Multiplication | * Additions, soustractions, opérations bit à bit, etc. * Décalages et rotations * Branchements |Opération flottante |} ===Les CPU superscalaires modernes=== Le premier CPU superscalaire grand public était le Pentium. Et c'était un CPU double émission qui n'implémentait pas la double émission entière-flottante, mais qui avait deux ALU entières. 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. Les CPU superscalaires modernes privilégient l'ajout d'ALU entières. Les multiplieurs n'ont pas été dupliqués, sauf sur les processeurs superscalaires très larges, car il est rare de devoir exécuter deux multiplications à la fois. Et c'est la même chose pour le ''barrel shifter''. La FPU aussi n'a pas été 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. ==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 simultanné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=== 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. Elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. La ''load/store queue'' reste une structure unique, mais on peut lui rajouter des ports. Et à ce peit jeu, il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire. [[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]] 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. Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre. Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Le Pentium 4 était dans ce cas, mais il était un peu seul dans sa catégorie. Un cas similaire est celui où l'unité mémoire est alimentée par une fenêtre de micro-opération mélangeant instructions mémoire et entières. Les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. Dans ce cas, l'émission multiple est rarement implémentée. Le cas suivant a une file pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique. Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui pouvaient seulement émettre une lecture en même temps qu'une écriture. Ils avaient deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse. [[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]] Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture demande d'utiliser une file d'écriture multiport, ce qui est assez compliqué. Implémenter des lectures en plus est plus simple : il suffit de rajouter une seconde file de µops LOAD et une unité de calcul, alimentées via un second port d'émission. Il est aussi possible de rajouter un second port de lecture au cache, mais ce n'est pas obligatoire. Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse. De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports de lecture et d'écriture car il y a plus de lecture que d'écritures. Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. : Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse. [[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]] Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettent plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail. ==Les optimisations des processeurs superscalaires larges== Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul. ===L'étape de chargement d'un CPU superscalaire large=== Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 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 autoaligné.]] Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements ! [[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]] Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez 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 microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total). Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois 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 microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement. Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations. ===La micro-fusion des processeurs Intel et AMD=== La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''. Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''. Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout. La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Le parallélisme mémoire | prevText=Le parallélisme mémoire | next=Exemples de microarchitectures CPU : le cas du x86 | nextText=Exemples de microarchitectures CPU : le cas du x86 }} </noinclude> 6dybil921hpk5l6vir5540gtlow1b6h 772782 772781 2026-09-21T23:00:00Z Mewtow 31375 /* La double émission entière-flottante */ 772782 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. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps. 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. 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. Deux branchements consécutifs sont quant à eux plus fréquents, mais exécuter deux branchements en même temps pose un problème : que faire quand les deux sont pris ? Deux accès mémoire consécutifs est assez rare. La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs sont systématiquement supportées, car dupliquer des ALU 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 et 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 possibels 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'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, 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. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant. 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. ===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 microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle. 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. ==L'évolution historique des processeurs superscalaires== L'évolution des CPU superscalaires historiques 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. 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. 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. Disons qu'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. Par exemple, 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. ===La double émission entière-flottante=== Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est d'émettre une micro-opération entière en même temps qu'une micro-opé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]] La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisations visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate]. 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'atterir 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|Emission multiple des opérations entières, implémentation courante]] Pour les branchements, deux solutions sont possibles. Avec la première, il y a émission parallèle des branchements, à savoir qu'il est possible d'émettre en branchement en parallèle de deux-trois instructions entières. Et cela impose la présence d'une unité de branchement séparée, avec son port d'émission. Avec la seconde, les branchements sont traités comme des instructions entières. Soit les branchements sont exécutés dans une ALU entière, soit il y a une unité de branchement reliée à un port d'émission pour les opérations entières. [[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Emission série et parallèle des branchements]] En général, les branchements sont exécutés comme des opérations entières. Cela permet d'économiser une unité de calcul et beaucoup de transistors dans l'unité d'émission/chargement/... {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 |- | * ALU entière * Multiplieur | * ALU entière * Barrel shifter * Unité de branchement | FPU |- | * Additions, soustractions, opérations bit à bit, etc. * Multiplication | * Additions, soustractions, opérations bit à bit, etc. * Décalages et rotations * Branchements |Opération flottante |} ===Les CPU superscalaires modernes=== Le premier CPU superscalaire grand public était le Pentium. Et c'était un CPU double émission qui n'implémentait pas la double émission entière-flottante, mais qui avait deux ALU entières. 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. Les CPU superscalaires modernes privilégient l'ajout d'ALU entières. Les multiplieurs n'ont pas été dupliqués, sauf sur les processeurs superscalaires très larges, car il est rare de devoir exécuter deux multiplications à la fois. Et c'est la même chose pour le ''barrel shifter''. La FPU aussi n'a pas été 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. ==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 simultanné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=== 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. Elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. La ''load/store queue'' reste une structure unique, mais on peut lui rajouter des ports. Et à ce peit jeu, il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire. [[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]] 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. Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre. Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Le Pentium 4 était dans ce cas, mais il était un peu seul dans sa catégorie. Un cas similaire est celui où l'unité mémoire est alimentée par une fenêtre de micro-opération mélangeant instructions mémoire et entières. Les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. Dans ce cas, l'émission multiple est rarement implémentée. Le cas suivant a une file pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique. Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui pouvaient seulement émettre une lecture en même temps qu'une écriture. Ils avaient deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse. [[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]] Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture demande d'utiliser une file d'écriture multiport, ce qui est assez compliqué. Implémenter des lectures en plus est plus simple : il suffit de rajouter une seconde file de µops LOAD et une unité de calcul, alimentées via un second port d'émission. Il est aussi possible de rajouter un second port de lecture au cache, mais ce n'est pas obligatoire. Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse. De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports de lecture et d'écriture car il y a plus de lecture que d'écritures. Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. : Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse. [[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]] Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettent plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail. ==Les optimisations des processeurs superscalaires larges== Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul. ===L'étape de chargement d'un CPU superscalaire large=== Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 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 autoaligné.]] Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements ! [[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]] Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez 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 microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total). Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois 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 microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement. Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations. ===La micro-fusion des processeurs Intel et AMD=== La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''. Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''. Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout. La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Le parallélisme mémoire | prevText=Le parallélisme mémoire | next=Exemples de microarchitectures CPU : le cas du x86 | nextText=Exemples de microarchitectures CPU : le cas du x86 }} </noinclude> 1lubztwcgzksi9s7pzg7wvbim9ydzk0 772783 772782 2026-09-21T23:04:24Z Mewtow 31375 /* L'évolution historique des processeurs superscalaires */ 772783 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. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps. 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. 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. Deux branchements consécutifs sont quant à eux plus fréquents, mais exécuter deux branchements en même temps pose un problème : que faire quand les deux sont pris ? Deux accès mémoire consécutifs est assez rare. La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs sont systématiquement supportées, car dupliquer des ALU 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 et 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 possibels 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'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, 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. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant. 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. ===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 microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle. 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 simultanné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=== 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. Elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. La ''load/store queue'' reste une structure unique, mais on peut lui rajouter des ports. Et à ce peit jeu, il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire. [[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]] 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. Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre. Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Le Pentium 4 était dans ce cas, mais il était un peu seul dans sa catégorie. Un cas similaire est celui où l'unité mémoire est alimentée par une fenêtre de micro-opération mélangeant instructions mémoire et entières. Les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. Dans ce cas, l'émission multiple est rarement implémentée. Le cas suivant a une file pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique. Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui pouvaient seulement émettre une lecture en même temps qu'une écriture. Ils avaient deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse. [[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]] Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture demande d'utiliser une file d'écriture multiport, ce qui est assez compliqué. Implémenter des lectures en plus est plus simple : il suffit de rajouter une seconde file de µops LOAD et une unité de calcul, alimentées via un second port d'émission. Il est aussi possible de rajouter un second port de lecture au cache, mais ce n'est pas obligatoire. Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse. De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports de lecture et d'écriture car il y a plus de lecture que d'écritures. Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. : Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse. [[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]] Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettent plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail. ==Les optimisations des processeurs superscalaires larges== Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul. ===L'étape de chargement d'un CPU superscalaire large=== Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 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 autoaligné.]] Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements ! [[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]] Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez 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 microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total). Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois 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 microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement. Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations. ===La micro-fusion des processeurs Intel et AMD=== La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''. Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''. Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout. La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Le parallélisme mémoire | prevText=Le parallélisme mémoire | next=Exemples de microarchitectures CPU : le cas du x86 | nextText=Exemples de microarchitectures CPU : le cas du x86 }} </noinclude> d1kjdfjuqyntaspvfp7321ymhu6sl6x 772784 772783 2026-09-21T23:04:33Z Mewtow 31375 /* L'implémentation des processeurs superscalaires */ 772784 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. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps. 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. 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. Deux branchements consécutifs sont quant à eux plus fréquents, mais exécuter deux branchements en même temps pose un problème : que faire quand les deux sont pris ? Deux accès mémoire consécutifs est assez rare. La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs sont systématiquement supportées, car dupliquer des ALU 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 et 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 possibels 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]] La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisations visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate]. 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'atterir 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|Emission multiple des opérations entières, implémentation courante]] Pour les branchements, deux solutions sont possibles. Avec la première, il y a émission parallèle des branchements, à savoir qu'il est possible d'émettre en branchement en parallèle de deux-trois instructions entières. Et cela impose la présence d'une unité de branchement séparée, avec son port d'émission. Avec la seconde, les branchements sont traités comme des instructions entières. Soit les branchements sont exécutés dans une ALU entière, soit il y a une unité de branchement reliée à un port d'émission pour les opérations entières. [[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Emission série et parallèle des branchements]] En général, les branchements sont exécutés comme des opérations entières. Cela permet d'économiser une unité de calcul et beaucoup de transistors dans l'unité d'émission/chargement/... {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 |- | * ALU entière * Multiplieur | * ALU entière * Barrel shifter * Unité de branchement | FPU |- | * Additions, soustractions, opérations bit à bit, etc. * Multiplication | * Additions, soustractions, opérations bit à bit, etc. * Décalages et rotations * Branchements |Opération flottante |} ===Les CPU superscalaires modernes=== Le premier CPU superscalaire grand public était le Pentium. Et c'était un CPU double émission qui n'implémentait pas la double émission entière-flottante, mais qui avait deux ALU entières. 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. Les CPU superscalaires modernes privilégient l'ajout d'ALU entières. Les multiplieurs n'ont pas été dupliqués, sauf sur les processeurs superscalaires très larges, car il est rare de devoir exécuter deux multiplications à la fois. Et c'est la même chose pour le ''barrel shifter''. La FPU aussi n'a pas été 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. ==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, 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. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant. 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. ===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 microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle. 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 simultanné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=== 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. Elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. La ''load/store queue'' reste une structure unique, mais on peut lui rajouter des ports. Et à ce peit jeu, il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire. [[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]] 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. Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre. Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Le Pentium 4 était dans ce cas, mais il était un peu seul dans sa catégorie. Un cas similaire est celui où l'unité mémoire est alimentée par une fenêtre de micro-opération mélangeant instructions mémoire et entières. Les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. Dans ce cas, l'émission multiple est rarement implémentée. Le cas suivant a une file pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique. Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui pouvaient seulement émettre une lecture en même temps qu'une écriture. Ils avaient deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse. [[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]] Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture demande d'utiliser une file d'écriture multiport, ce qui est assez compliqué. Implémenter des lectures en plus est plus simple : il suffit de rajouter une seconde file de µops LOAD et une unité de calcul, alimentées via un second port d'émission. Il est aussi possible de rajouter un second port de lecture au cache, mais ce n'est pas obligatoire. Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse. De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports de lecture et d'écriture car il y a plus de lecture que d'écritures. Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. : Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse. [[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]] Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettent plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail. ==Les optimisations des processeurs superscalaires larges== Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul. ===L'étape de chargement d'un CPU superscalaire large=== Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 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 autoaligné.]] Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements ! [[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]] Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez 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 microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total). Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois 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 microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement. Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations. ===La micro-fusion des processeurs Intel et AMD=== La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''. Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''. Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout. La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Le parallélisme mémoire | prevText=Le parallélisme mémoire | next=Exemples de microarchitectures CPU : le cas du x86 | nextText=Exemples de microarchitectures CPU : le cas du x86 }} </noinclude> 7d6dpwsxq63ohbgs9oqj3jccda5fz63 772785 772784 2026-09-21T23:09:16Z Mewtow 31375 /* L'émission multiple des micro-opérations entières */ 772785 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. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps. 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. 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. Deux branchements consécutifs sont quant à eux plus fréquents, mais exécuter deux branchements en même temps pose un problème : que faire quand les deux sont pris ? Deux accès mémoire consécutifs est assez rare. La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs sont systématiquement supportées, car dupliquer des ALU 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 et 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 possibels 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]] La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisations visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate]. 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'atterir 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|Emission multiple des opérations entières, implémentation courante]] Pour les branchements, deux solutions sont possibles. Avec la première, il y a émission parallèle des branchements, à savoir qu'il est possible d'émettre en branchement en parallèle de deux-trois instructions entières. Et cela impose la présence d'une unité de branchement séparée, avec son port d'émission. Avec la seconde, les branchements sont traités comme des instructions entières. Soit les branchements sont exécutés dans une ALU entière, soit il y a une unité de branchement reliée à un port d'émission pour les opérations entières. [[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Emission série et parallèle des branchements]] En général, les branchements sont exécutés comme des opérations entières. Cela permet d'économiser une unité de calcul et beaucoup de transistors dans l'unité d'émission/chargement/... {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 |- | * ALU entière * Multiplieur | * ALU entière * Barrel shifter * Unité de branchement * Unité mémoire | FPU |- | * Additions, soustractions, opérations bit à bit, etc. * Multiplication | * Additions, soustractions, opérations bit à bit, etc. * Décalages et rotations * Branchements * µops mémoire. |Opération flottante |} ===Les CPU superscalaires modernes=== Le premier CPU superscalaire grand public était le Pentium. Et c'était un CPU double émission qui n'implémentait pas la double émission entière-flottante, mais qui avait deux ALU entières. 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. Les CPU superscalaires modernes privilégient l'ajout d'ALU entières. Les multiplieurs n'ont pas été dupliqués, sauf sur les processeurs superscalaires très larges, car il est rare de devoir exécuter deux multiplications à la fois. Et c'est la même chose pour le ''barrel shifter''. La FPU aussi n'a pas été 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. ==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, 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. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant. 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. ===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 microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle. 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 simultanné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=== 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. Elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. La ''load/store queue'' reste une structure unique, mais on peut lui rajouter des ports. Et à ce peit jeu, il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire. [[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]] 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. Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre. Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Le Pentium 4 était dans ce cas, mais il était un peu seul dans sa catégorie. Un cas similaire est celui où l'unité mémoire est alimentée par une fenêtre de micro-opération mélangeant instructions mémoire et entières. Les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. Dans ce cas, l'émission multiple est rarement implémentée. Le cas suivant a une file pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique. Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui pouvaient seulement émettre une lecture en même temps qu'une écriture. Ils avaient deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse. [[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]] Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture demande d'utiliser une file d'écriture multiport, ce qui est assez compliqué. Implémenter des lectures en plus est plus simple : il suffit de rajouter une seconde file de µops LOAD et une unité de calcul, alimentées via un second port d'émission. Il est aussi possible de rajouter un second port de lecture au cache, mais ce n'est pas obligatoire. Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse. De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports de lecture et d'écriture car il y a plus de lecture que d'écritures. Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. : Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse. [[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]] Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettent plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail. ==Les optimisations des processeurs superscalaires larges== Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul. ===L'étape de chargement d'un CPU superscalaire large=== Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 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 autoaligné.]] Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements ! [[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]] Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez 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 microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total). Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois 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 microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement. Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations. ===La micro-fusion des processeurs Intel et AMD=== La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''. Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''. Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout. La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Le parallélisme mémoire | prevText=Le parallélisme mémoire | next=Exemples de microarchitectures CPU : le cas du x86 | nextText=Exemples de microarchitectures CPU : le cas du x86 }} </noinclude> im013v6anqiux6vchtpln0kvoqw72ul 772786 772785 2026-09-21T23:09:25Z Mewtow 31375 /* L'émission multiple des micro-opérations entières */ 772786 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. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps. 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. 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. Deux branchements consécutifs sont quant à eux plus fréquents, mais exécuter deux branchements en même temps pose un problème : que faire quand les deux sont pris ? Deux accès mémoire consécutifs est assez rare. La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs sont systématiquement supportées, car dupliquer des ALU 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 et 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 possibels 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]] La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisations visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate]. 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'atterir 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|Emission multiple des opérations entières, implémentation courante]] Pour les branchements, deux solutions sont possibles. Avec la première, il y a émission parallèle des branchements, à savoir qu'il est possible d'émettre en branchement en parallèle de deux-trois instructions entières. Et cela impose la présence d'une unité de branchement séparée, avec son port d'émission. Avec la seconde, les branchements sont traités comme des instructions entières. Soit les branchements sont exécutés dans une ALU entière, soit il y a une unité de branchement reliée à un port d'émission pour les opérations entières. [[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Emission série et parallèle des branchements]] En général, les branchements sont exécutés comme des opérations entières. Cela permet d'économiser une unité de calcul et beaucoup de transistors dans l'unité d'émission/chargement/... {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 |- | * ALU entière * Multiplieur | * ALU entière * Barrel shifter * Unité de branchement * Unité mémoire | FPU |- | * Additions, soustractions, opérations bit à bit, etc. * Multiplication | * Additions, soustractions, opérations bit à bit, etc. * Décalages et rotations * Branchements * µops mémoire. | Opération flottante |} ===Les CPU superscalaires modernes=== Le premier CPU superscalaire grand public était le Pentium. Et c'était un CPU double émission qui n'implémentait pas la double émission entière-flottante, mais qui avait deux ALU entières. 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. Les CPU superscalaires modernes privilégient l'ajout d'ALU entières. Les multiplieurs n'ont pas été dupliqués, sauf sur les processeurs superscalaires très larges, car il est rare de devoir exécuter deux multiplications à la fois. Et c'est la même chose pour le ''barrel shifter''. La FPU aussi n'a pas été 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. ==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, 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. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant. 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. ===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 microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle. 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 simultanné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=== 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. Elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. La ''load/store queue'' reste une structure unique, mais on peut lui rajouter des ports. Et à ce peit jeu, il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire. [[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]] 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. Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre. Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Le Pentium 4 était dans ce cas, mais il était un peu seul dans sa catégorie. Un cas similaire est celui où l'unité mémoire est alimentée par une fenêtre de micro-opération mélangeant instructions mémoire et entières. Les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. Dans ce cas, l'émission multiple est rarement implémentée. Le cas suivant a une file pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique. Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui pouvaient seulement émettre une lecture en même temps qu'une écriture. Ils avaient deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse. [[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]] Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture demande d'utiliser une file d'écriture multiport, ce qui est assez compliqué. Implémenter des lectures en plus est plus simple : il suffit de rajouter une seconde file de µops LOAD et une unité de calcul, alimentées via un second port d'émission. Il est aussi possible de rajouter un second port de lecture au cache, mais ce n'est pas obligatoire. Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse. De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports de lecture et d'écriture car il y a plus de lecture que d'écritures. Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. : Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse. [[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]] Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettent plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail. ==Les optimisations des processeurs superscalaires larges== Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul. ===L'étape de chargement d'un CPU superscalaire large=== Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 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 autoaligné.]] Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements ! [[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]] Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez 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 microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total). Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois 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 microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement. Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations. ===La micro-fusion des processeurs Intel et AMD=== La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''. Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''. Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout. La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Le parallélisme mémoire | prevText=Le parallélisme mémoire | next=Exemples de microarchitectures CPU : le cas du x86 | nextText=Exemples de microarchitectures CPU : le cas du x86 }} </noinclude> 2wv3k0bqyzir7hg1x6hcpywambmnrvn 772787 772786 2026-09-21T23:12:44Z Mewtow 31375 /* L'émission multiple des micro-opérations entières */ 772787 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. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps. 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. 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. Deux branchements consécutifs sont quant à eux plus fréquents, mais exécuter deux branchements en même temps pose un problème : que faire quand les deux sont pris ? Deux accès mémoire consécutifs est assez rare. La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs sont systématiquement supportées, car dupliquer des ALU 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 et 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 possibels 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]] La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisations visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate]. 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'atterir 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|Emission multiple des opérations entières, implémentation courante]] Pour les branchements, deux solutions sont possibles. Avec la première, il y a émission parallèle des branchements, à savoir qu'il est possible d'émettre en branchement en parallèle de deux-trois instructions entières.Avec la seconde, les branchements sont traités comme des instructions entières. {|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|Emission série et parallèle des branchements]] ===Les CPU superscalaires modernes=== Le premier CPU superscalaire grand public était le Pentium. Et c'était un CPU double émission qui n'implémentait pas la double émission entière-flottante, mais qui avait deux ALU entières. 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. Les CPU superscalaires modernes privilégient l'ajout d'ALU entières. Les multiplieurs n'ont pas été dupliqués, sauf sur les processeurs superscalaires très larges, car il est rare de devoir exécuter deux multiplications à la fois. Et c'est la même chose pour le ''barrel shifter''. La FPU aussi n'a pas été 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. ==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, 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. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant. 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. ===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 microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle. 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 simultanné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=== 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. Elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. La ''load/store queue'' reste une structure unique, mais on peut lui rajouter des ports. Et à ce peit jeu, il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire. [[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]] 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. Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre. Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Le Pentium 4 était dans ce cas, mais il était un peu seul dans sa catégorie. Un cas similaire est celui où l'unité mémoire est alimentée par une fenêtre de micro-opération mélangeant instructions mémoire et entières. Les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. Dans ce cas, l'émission multiple est rarement implémentée. Le cas suivant a une file pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique. Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui pouvaient seulement émettre une lecture en même temps qu'une écriture. Ils avaient deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse. [[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]] Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture demande d'utiliser une file d'écriture multiport, ce qui est assez compliqué. Implémenter des lectures en plus est plus simple : il suffit de rajouter une seconde file de µops LOAD et une unité de calcul, alimentées via un second port d'émission. Il est aussi possible de rajouter un second port de lecture au cache, mais ce n'est pas obligatoire. Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse. De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports de lecture et d'écriture car il y a plus de lecture que d'écritures. Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. : Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse. [[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]] Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettent plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail. ==Les optimisations des processeurs superscalaires larges== Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul. ===L'étape de chargement d'un CPU superscalaire large=== Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 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 autoaligné.]] Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements ! [[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]] Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez 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 microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total). Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois 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 microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement. Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations. ===La micro-fusion des processeurs Intel et AMD=== La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''. Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''. Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout. La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Le parallélisme mémoire | prevText=Le parallélisme mémoire | next=Exemples de microarchitectures CPU : le cas du x86 | nextText=Exemples de microarchitectures CPU : le cas du x86 }} </noinclude> qz5mxvkywup2uvcnx25m8bxloyg10ia 772788 772787 2026-09-21T23:13:26Z Mewtow 31375 /* L'émission multiple des micro-opérations entières */ 772788 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. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps. 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. 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. Deux branchements consécutifs sont quant à eux plus fréquents, mais exécuter deux branchements en même temps pose un problème : que faire quand les deux sont pris ? Deux accès mémoire consécutifs est assez rare. La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs sont systématiquement supportées, car dupliquer des ALU 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 et 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 possibels 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]] La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisations visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate]. 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'atterir 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|Emission multiple des opérations entières, implémentation courante]] Pour les branchements, deux solutions sont possibles. Avec la première, il y a émission parallèle des branchements, à savoir qu'il est possible d'émettre en branchement en parallèle de deux-trois instructions entières. 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|Emission série et parallèle des branchements]] ===Les CPU superscalaires modernes=== Le premier CPU superscalaire grand public était le Pentium. Et c'était un CPU double émission qui n'implémentait pas la double émission entière-flottante, mais qui avait deux ALU entières. 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. Les CPU superscalaires modernes privilégient l'ajout d'ALU entières. Les multiplieurs n'ont pas été dupliqués, sauf sur les processeurs superscalaires très larges, car il est rare de devoir exécuter deux multiplications à la fois. Et c'est la même chose pour le ''barrel shifter''. La FPU aussi n'a pas été 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. ==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, 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. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant. 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. ===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 microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle. 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 simultanné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=== 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. Elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. La ''load/store queue'' reste une structure unique, mais on peut lui rajouter des ports. Et à ce peit jeu, il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire. [[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]] 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. Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre. Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Le Pentium 4 était dans ce cas, mais il était un peu seul dans sa catégorie. Un cas similaire est celui où l'unité mémoire est alimentée par une fenêtre de micro-opération mélangeant instructions mémoire et entières. Les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. Dans ce cas, l'émission multiple est rarement implémentée. Le cas suivant a une file pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique. Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui pouvaient seulement émettre une lecture en même temps qu'une écriture. Ils avaient deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse. [[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]] Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture demande d'utiliser une file d'écriture multiport, ce qui est assez compliqué. Implémenter des lectures en plus est plus simple : il suffit de rajouter une seconde file de µops LOAD et une unité de calcul, alimentées via un second port d'émission. Il est aussi possible de rajouter un second port de lecture au cache, mais ce n'est pas obligatoire. Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse. De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports de lecture et d'écriture car il y a plus de lecture que d'écritures. Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. : Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse. [[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]] Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettent plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail. ==Les optimisations des processeurs superscalaires larges== Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul. ===L'étape de chargement d'un CPU superscalaire large=== Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 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 autoaligné.]] Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements ! [[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]] Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez 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 microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total). Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois 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 microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement. Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations. ===La micro-fusion des processeurs Intel et AMD=== La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''. Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''. Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout. La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Le parallélisme mémoire | prevText=Le parallélisme mémoire | next=Exemples de microarchitectures CPU : le cas du x86 | nextText=Exemples de microarchitectures CPU : le cas du x86 }} </noinclude> r73nx6wqdcm4bp06thowe417ox11guc 772789 772788 2026-09-21T23:14:01Z Mewtow 31375 /* Les CPU superscalaires modernes */ 772789 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. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps. 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. 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. Deux branchements consécutifs sont quant à eux plus fréquents, mais exécuter deux branchements en même temps pose un problème : que faire quand les deux sont pris ? Deux accès mémoire consécutifs est assez rare. La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs sont systématiquement supportées, car dupliquer des ALU 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 et 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 possibels 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]] La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisations visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate]. 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'atterir 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|Emission multiple des opérations entières, implémentation courante]] Pour les branchements, deux solutions sont possibles. Avec la première, il y a émission parallèle des branchements, à savoir qu'il est possible d'émettre en branchement en parallèle de deux-trois instructions entières. 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|Emission 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, 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. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant. 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. ===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 microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle. 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 simultanné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=== 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. Elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. La ''load/store queue'' reste une structure unique, mais on peut lui rajouter des ports. Et à ce peit jeu, il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire. [[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]] 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. Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre. Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Le Pentium 4 était dans ce cas, mais il était un peu seul dans sa catégorie. Un cas similaire est celui où l'unité mémoire est alimentée par une fenêtre de micro-opération mélangeant instructions mémoire et entières. Les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. Dans ce cas, l'émission multiple est rarement implémentée. Le cas suivant a une file pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique. Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui pouvaient seulement émettre une lecture en même temps qu'une écriture. Ils avaient deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse. [[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]] Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture demande d'utiliser une file d'écriture multiport, ce qui est assez compliqué. Implémenter des lectures en plus est plus simple : il suffit de rajouter une seconde file de µops LOAD et une unité de calcul, alimentées via un second port d'émission. Il est aussi possible de rajouter un second port de lecture au cache, mais ce n'est pas obligatoire. Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse. De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports de lecture et d'écriture car il y a plus de lecture que d'écritures. Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. : Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse. [[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]] Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettent plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail. ==Les optimisations des processeurs superscalaires larges== Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul. ===L'étape de chargement d'un CPU superscalaire large=== Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 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 autoaligné.]] Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements ! [[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]] Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez 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 microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total). Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois 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 microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement. Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations. ===La micro-fusion des processeurs Intel et AMD=== La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''. Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''. Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout. La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Le parallélisme mémoire | prevText=Le parallélisme mémoire | next=Exemples de microarchitectures CPU : le cas du x86 | nextText=Exemples de microarchitectures CPU : le cas du x86 }} </noinclude> 3zcrkhkml1zty8f2resothznium256g 772790 772789 2026-09-21T23:16:49Z Mewtow 31375 /* Les unités de calcul d'un processeur superscalaire */ 772790 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. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps. 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. 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. Deux branchements consécutifs sont quant à eux plus fréquents, mais exécuter deux branchements en même temps pose un problème : que faire quand les deux sont pris ? Deux accès mémoire consécutifs est assez rare. La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs sont systématiquement supportées, car dupliquer des ALU 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 et 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 possibels 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]] La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisations visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate]. 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'atterir 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|Emission multiple des opérations entières, implémentation courante]] Pour les branchements, deux solutions sont possibles. Avec la première, il y a émission parallèle des branchements, à savoir qu'il est possible d'émettre en branchement en parallèle de deux-trois instructions entières. 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|Emission 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. 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. Les CPU superscalaires modernes privilégient l'ajout d'ALU entières. Les multiplieurs n'ont pas été dupliqués, sauf sur les processeurs superscalaires très larges, car il est rare de devoir exécuter deux multiplications à la fois. Et c'est la même chose pour le ''barrel shifter''. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant. 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. 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. ===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 microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle. 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 simultanné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=== 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. Elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. La ''load/store queue'' reste une structure unique, mais on peut lui rajouter des ports. Et à ce peit jeu, il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire. [[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]] 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. Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre. Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Le Pentium 4 était dans ce cas, mais il était un peu seul dans sa catégorie. Un cas similaire est celui où l'unité mémoire est alimentée par une fenêtre de micro-opération mélangeant instructions mémoire et entières. Les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. Dans ce cas, l'émission multiple est rarement implémentée. Le cas suivant a une file pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique. Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui pouvaient seulement émettre une lecture en même temps qu'une écriture. Ils avaient deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse. [[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]] Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture demande d'utiliser une file d'écriture multiport, ce qui est assez compliqué. Implémenter des lectures en plus est plus simple : il suffit de rajouter une seconde file de µops LOAD et une unité de calcul, alimentées via un second port d'émission. Il est aussi possible de rajouter un second port de lecture au cache, mais ce n'est pas obligatoire. Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse. De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports de lecture et d'écriture car il y a plus de lecture que d'écritures. Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. : Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse. [[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]] Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettent plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail. ==Les optimisations des processeurs superscalaires larges== Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul. ===L'étape de chargement d'un CPU superscalaire large=== Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 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 autoaligné.]] Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements ! [[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]] Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez 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 microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total). Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois 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 microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement. Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations. ===La micro-fusion des processeurs Intel et AMD=== La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''. Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''. Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout. La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Le parallélisme mémoire | prevText=Le parallélisme mémoire | next=Exemples de microarchitectures CPU : le cas du x86 | nextText=Exemples de microarchitectures CPU : le cas du x86 }} </noinclude> 6zd7s9hd6yl9whw1u1pdblupj8or1lu 772791 772790 2026-09-21T23:19:14Z Mewtow 31375 /* Les unités de calcul d'un processeur superscalaire */ 772791 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. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps. 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. 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. Deux branchements consécutifs sont quant à eux plus fréquents, mais exécuter deux branchements en même temps pose un problème : que faire quand les deux sont pris ? Deux accès mémoire consécutifs est assez rare. La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs sont systématiquement supportées, car dupliquer des ALU 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 et 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 possibels 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]] La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisations visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate]. 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'atterir 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|Emission multiple des opérations entières, implémentation courante]] Pour les branchements, deux solutions sont possibles. Avec la première, il y a émission parallèle des branchements, à savoir qu'il est possible d'émettre en branchement en parallèle de deux-trois instructions entières. 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|Emission 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. 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. ===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 microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle. 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 simultanné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=== 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. Elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. La ''load/store queue'' reste une structure unique, mais on peut lui rajouter des ports. Et à ce peit jeu, il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire. [[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]] 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. Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre. Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Le Pentium 4 était dans ce cas, mais il était un peu seul dans sa catégorie. Un cas similaire est celui où l'unité mémoire est alimentée par une fenêtre de micro-opération mélangeant instructions mémoire et entières. Les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. Dans ce cas, l'émission multiple est rarement implémentée. Le cas suivant a une file pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique. Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui pouvaient seulement émettre une lecture en même temps qu'une écriture. Ils avaient deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse. [[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]] Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture demande d'utiliser une file d'écriture multiport, ce qui est assez compliqué. Implémenter des lectures en plus est plus simple : il suffit de rajouter une seconde file de µops LOAD et une unité de calcul, alimentées via un second port d'émission. Il est aussi possible de rajouter un second port de lecture au cache, mais ce n'est pas obligatoire. Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse. De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports de lecture et d'écriture car il y a plus de lecture que d'écritures. Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. : Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse. [[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]] Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettent plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail. ==Les optimisations des processeurs superscalaires larges== Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul. ===L'étape de chargement d'un CPU superscalaire large=== Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 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 autoaligné.]] Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements ! [[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]] Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez 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 microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total). Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois 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 microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement. Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations. ===La micro-fusion des processeurs Intel et AMD=== La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''. Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''. Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout. La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Le parallélisme mémoire | prevText=Le parallélisme mémoire | next=Exemples de microarchitectures CPU : le cas du x86 | nextText=Exemples de microarchitectures CPU : le cas du x86 }} </noinclude> h6niz5itpu8bo4f3hgx5pb38szwy3a3 772792 772791 2026-09-21T23:33:10Z Mewtow 31375 /* Les unités de calcul d'un processeur superscalaire */ 772792 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. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps. 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. 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. Deux branchements consécutifs sont quant à eux plus fréquents, mais exécuter deux branchements en même temps pose un problème : que faire quand les deux sont pris ? Deux accès mémoire consécutifs est assez rare. La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs sont systématiquement supportées, car dupliquer des ALU 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 et 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 possibels 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]] La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisations visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate]. 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'atterir 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|Emission multiple des opérations entières, implémentation courante]] Pour les branchements, deux solutions sont possibles. Avec la première, il y a émission parallèle des branchements, à savoir qu'il est possible d'émettre en branchement en parallèle de deux-trois instructions entières. 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|Emission 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 microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle. 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 simultanné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=== 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. Elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. La ''load/store queue'' reste une structure unique, mais on peut lui rajouter des ports. Et à ce peit jeu, il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire. [[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]] 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. Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre. Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Le Pentium 4 était dans ce cas, mais il était un peu seul dans sa catégorie. Un cas similaire est celui où l'unité mémoire est alimentée par une fenêtre de micro-opération mélangeant instructions mémoire et entières. Les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. Dans ce cas, l'émission multiple est rarement implémentée. Le cas suivant a une file pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique. Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui pouvaient seulement émettre une lecture en même temps qu'une écriture. Ils avaient deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse. [[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]] Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture demande d'utiliser une file d'écriture multiport, ce qui est assez compliqué. Implémenter des lectures en plus est plus simple : il suffit de rajouter une seconde file de µops LOAD et une unité de calcul, alimentées via un second port d'émission. Il est aussi possible de rajouter un second port de lecture au cache, mais ce n'est pas obligatoire. Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse. De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports de lecture et d'écriture car il y a plus de lecture que d'écritures. Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. : Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse. [[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]] Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettent plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail. ==Les optimisations des processeurs superscalaires larges== Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul. ===L'étape de chargement d'un CPU superscalaire large=== Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 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 autoaligné.]] Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements ! [[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]] Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez 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 microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total). Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois 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 microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement. Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations. ===La micro-fusion des processeurs Intel et AMD=== La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''. Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''. Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout. La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Le parallélisme mémoire | prevText=Le parallélisme mémoire | next=Exemples de microarchitectures CPU : le cas du x86 | nextText=Exemples de microarchitectures CPU : le cas du x86 }} </noinclude> mn9bmwuyp6fz7qncjfig8ed2ijjp97f 772793 772792 2026-09-21T23:40:26Z Mewtow 31375 /* Les contraintes d’appariement */ 772793 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. 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. Deux branchements consécutifs sont quant à eux plus fréquents, mais exécuter deux branchements en même temps pose un problème : que faire quand les deux sont pris ? Deux accès mémoire consécutifs est assez rare. La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs sont systématiquement supportées, car dupliquer des ALU 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 et 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 possibels 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]] La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisations visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate]. 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'atterir 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|Emission multiple des opérations entières, implémentation courante]] Pour les branchements, deux solutions sont possibles. Avec la première, il y a émission parallèle des branchements, à savoir qu'il est possible d'émettre en branchement en parallèle de deux-trois instructions entières. 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|Emission 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 microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle. 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 simultanné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=== 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. Elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. La ''load/store queue'' reste une structure unique, mais on peut lui rajouter des ports. Et à ce peit jeu, il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire. [[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]] 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. Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre. Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Le Pentium 4 était dans ce cas, mais il était un peu seul dans sa catégorie. Un cas similaire est celui où l'unité mémoire est alimentée par une fenêtre de micro-opération mélangeant instructions mémoire et entières. Les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. Dans ce cas, l'émission multiple est rarement implémentée. Le cas suivant a une file pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique. Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui pouvaient seulement émettre une lecture en même temps qu'une écriture. Ils avaient deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse. [[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]] Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture demande d'utiliser une file d'écriture multiport, ce qui est assez compliqué. Implémenter des lectures en plus est plus simple : il suffit de rajouter une seconde file de µops LOAD et une unité de calcul, alimentées via un second port d'émission. Il est aussi possible de rajouter un second port de lecture au cache, mais ce n'est pas obligatoire. Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse. De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports de lecture et d'écriture car il y a plus de lecture que d'écritures. Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. : Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse. [[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]] Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettent plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail. ==Les optimisations des processeurs superscalaires larges== Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul. ===L'étape de chargement d'un CPU superscalaire large=== Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 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 autoaligné.]] Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements ! [[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]] Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez 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 microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total). Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois 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 microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement. Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations. ===La micro-fusion des processeurs Intel et AMD=== La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''. Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''. Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout. La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Le parallélisme mémoire | prevText=Le parallélisme mémoire | next=Exemples de microarchitectures CPU : le cas du x86 | nextText=Exemples de microarchitectures CPU : le cas du x86 }} </noinclude> sjdpbrcaqgnbm0ejfcuzgixoh9hb751 772794 772793 2026-09-21T23:42:51Z Mewtow 31375 /* Les contraintes d’appariement */ 772794 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. 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, exu 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 possibels 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]] La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisations visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate]. 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'atterir 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|Emission multiple des opérations entières, implémentation courante]] Pour les branchements, deux solutions sont possibles. Avec la première, il y a émission parallèle des branchements, à savoir qu'il est possible d'émettre en branchement en parallèle de deux-trois instructions entières. 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|Emission 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 microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle. 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 simultanné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=== 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. Elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. La ''load/store queue'' reste une structure unique, mais on peut lui rajouter des ports. Et à ce peit jeu, il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire. [[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]] 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. Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre. Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Le Pentium 4 était dans ce cas, mais il était un peu seul dans sa catégorie. Un cas similaire est celui où l'unité mémoire est alimentée par une fenêtre de micro-opération mélangeant instructions mémoire et entières. Les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. Dans ce cas, l'émission multiple est rarement implémentée. Le cas suivant a une file pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique. Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui pouvaient seulement émettre une lecture en même temps qu'une écriture. Ils avaient deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse. [[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]] Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture demande d'utiliser une file d'écriture multiport, ce qui est assez compliqué. Implémenter des lectures en plus est plus simple : il suffit de rajouter une seconde file de µops LOAD et une unité de calcul, alimentées via un second port d'émission. Il est aussi possible de rajouter un second port de lecture au cache, mais ce n'est pas obligatoire. Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse. De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports de lecture et d'écriture car il y a plus de lecture que d'écritures. Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. : Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse. [[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]] Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettent plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail. ==Les optimisations des processeurs superscalaires larges== Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul. ===L'étape de chargement d'un CPU superscalaire large=== Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 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 autoaligné.]] Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements ! [[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]] Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez 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 microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total). Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois 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 microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement. Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations. ===La micro-fusion des processeurs Intel et AMD=== La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''. Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''. Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout. La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Le parallélisme mémoire | prevText=Le parallélisme mémoire | next=Exemples de microarchitectures CPU : le cas du x86 | nextText=Exemples de microarchitectures CPU : le cas du x86 }} </noinclude> 2nmuwn9y2rl6ja7vdzsj9af5sk194sk 772795 772794 2026-09-21T23:45:42Z Mewtow 31375 /* La double émission entière-flottante */ 772795 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. 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, exu 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 possibels 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]] La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le moteur graphique de Quake a été optimisé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Et une bonne partie de ses optimisations (en assembleur) visait à mixer opérations INT et FLOAT, de manière à utiliser au mieux la double émission entière-flottante. le résultat est que le nombre de FPS était doublé ! Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate]. 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'atterir 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|Emission multiple des opérations entières, implémentation courante]] Pour les branchements, deux solutions sont possibles. Avec la première, il y a émission parallèle des branchements, à savoir qu'il est possible d'émettre en branchement en parallèle de deux-trois instructions entières. 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|Emission 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 microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle. 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 simultanné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=== 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. Elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. La ''load/store queue'' reste une structure unique, mais on peut lui rajouter des ports. Et à ce peit jeu, il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire. [[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]] 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. Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre. Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Le Pentium 4 était dans ce cas, mais il était un peu seul dans sa catégorie. Un cas similaire est celui où l'unité mémoire est alimentée par une fenêtre de micro-opération mélangeant instructions mémoire et entières. Les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. Dans ce cas, l'émission multiple est rarement implémentée. Le cas suivant a une file pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique. Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui pouvaient seulement émettre une lecture en même temps qu'une écriture. Ils avaient deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse. [[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]] Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture demande d'utiliser une file d'écriture multiport, ce qui est assez compliqué. Implémenter des lectures en plus est plus simple : il suffit de rajouter une seconde file de µops LOAD et une unité de calcul, alimentées via un second port d'émission. Il est aussi possible de rajouter un second port de lecture au cache, mais ce n'est pas obligatoire. Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse. De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports de lecture et d'écriture car il y a plus de lecture que d'écritures. Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. : Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse. [[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]] Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettent plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail. ==Les optimisations des processeurs superscalaires larges== Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul. ===L'étape de chargement d'un CPU superscalaire large=== Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 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 autoaligné.]] Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements ! [[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]] Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez 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 microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total). Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois 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 microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement. Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations. ===La micro-fusion des processeurs Intel et AMD=== La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''. Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''. Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout. La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Le parallélisme mémoire | prevText=Le parallélisme mémoire | next=Exemples de microarchitectures CPU : le cas du x86 | nextText=Exemples de microarchitectures CPU : le cas du x86 }} </noinclude> d0i1pteo5gc6o39j3v9i6zynio8cs0h 772796 772795 2026-09-21T23:48:02Z Mewtow 31375 /* La double émission entière-flottante */ 772796 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. 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, exu 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 possibels 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'atterir 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|Emission multiple des opérations entières, implémentation courante]] Pour les branchements, deux solutions sont possibles. Avec la première, il y a émission parallèle des branchements, à savoir qu'il est possible d'émettre en branchement en parallèle de deux-trois instructions entières. 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|Emission 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 microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle. 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 simultanné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=== 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. Elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. La ''load/store queue'' reste une structure unique, mais on peut lui rajouter des ports. Et à ce peit jeu, il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire. [[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]] 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. Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre. Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Le Pentium 4 était dans ce cas, mais il était un peu seul dans sa catégorie. Un cas similaire est celui où l'unité mémoire est alimentée par une fenêtre de micro-opération mélangeant instructions mémoire et entières. Les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. Dans ce cas, l'émission multiple est rarement implémentée. Le cas suivant a une file pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique. Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui pouvaient seulement émettre une lecture en même temps qu'une écriture. Ils avaient deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse. [[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]] Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture demande d'utiliser une file d'écriture multiport, ce qui est assez compliqué. Implémenter des lectures en plus est plus simple : il suffit de rajouter une seconde file de µops LOAD et une unité de calcul, alimentées via un second port d'émission. Il est aussi possible de rajouter un second port de lecture au cache, mais ce n'est pas obligatoire. Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse. De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports de lecture et d'écriture car il y a plus de lecture que d'écritures. Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. : Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse. [[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]] Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettent plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail. ==Les optimisations des processeurs superscalaires larges== Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul. ===L'étape de chargement d'un CPU superscalaire large=== Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 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 autoaligné.]] Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements ! [[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]] Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez 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 microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total). Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois 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 microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement. Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations. ===La micro-fusion des processeurs Intel et AMD=== La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''. Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''. Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout. La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Le parallélisme mémoire | prevText=Le parallélisme mémoire | next=Exemples de microarchitectures CPU : le cas du x86 | nextText=Exemples de microarchitectures CPU : le cas du x86 }} </noinclude> imat9awcs7xci8m6xyhe4kurc5lts96 772797 772796 2026-09-21T23:59:17Z Mewtow 31375 /* L'émission multiple des accès mémoire */ 772797 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. 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, exu 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 possibels 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'atterir 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|Emission multiple des opérations entières, implémentation courante]] Pour les branchements, deux solutions sont possibles. Avec la première, il y a émission parallèle des branchements, à savoir qu'il est possible d'émettre en branchement en parallèle de deux-trois instructions entières. 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|Emission 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 microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle. 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 simultanné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=== 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=2.5|Emission 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. De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports de lecture et d'écriture car il y a plus de lecture que d'écritures. Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettent plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail. Pour rappel, l'unité mémoire est précédée par une structure 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 particlier 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 autoaligné.]] Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements ! [[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]] Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez 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 microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total). Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois 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 microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement. Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations. ===La micro-fusion des processeurs Intel et AMD=== La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''. Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''. Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout. La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Le parallélisme mémoire | prevText=Le parallélisme mémoire | next=Exemples de microarchitectures CPU : le cas du x86 | nextText=Exemples de microarchitectures CPU : le cas du x86 }} </noinclude> q2ug0wrojlelex8b80kf92zr3kblm7w 772798 772797 2026-09-22T00:05:24Z Mewtow 31375 /* L'émission multiple des accès mémoire */ 772798 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. 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, exu 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 possibels 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'atterir 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|Emission multiple des opérations entières, implémentation courante]] Pour les branchements, deux solutions sont possibles. Avec la première, il y a émission parallèle des branchements, à savoir qu'il est possible d'émettre en branchement en parallèle de deux-trois instructions entières. 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|Emission 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 microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle. 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 simultanné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=2.5|Emission 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 microarchitecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet d'émettre deux lectures en même temps qu'une écriture. [[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]] Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettent plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail. Pour rappel, l'unité mémoire est précédée par une structure 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 particlier 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:Emission LOAD - Store séparée.png|thumb|Emission LOAD - Store séparée]] ==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 autoaligné.]] Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements ! [[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]] Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez 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 microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total). Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois 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 microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement. Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations. ===La micro-fusion des processeurs Intel et AMD=== La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''. Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''. Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout. La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Le parallélisme mémoire | prevText=Le parallélisme mémoire | next=Exemples de microarchitectures CPU : le cas du x86 | nextText=Exemples de microarchitectures CPU : le cas du x86 }} </noinclude> c1gwj7sc068wdx57z0bl0bc4ezx6qs7 772799 772798 2026-09-22T00:05:43Z Mewtow 31375 772799 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. 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, exu 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 possibels 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'atterir 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|Emission multiple des opérations entières, implémentation courante]] Pour les branchements, deux solutions sont possibles. Avec la première, il y a émission parallèle des branchements, à savoir qu'il est possible d'émettre en branchement en parallèle de deux-trois instructions entières. 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|Emission 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 microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle. 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 simultanné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=2.5|Emission 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 microarchitecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet d'émettre deux lectures en même temps qu'une écriture. [[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]] Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettent plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail. Pour rappel, l'unité mémoire est précédée par une structure 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 particlier 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:Emission LOAD - Store séparée.png|thumb|Emission LOAD - Store séparée]] ==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 autoaligné.]] Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements ! [[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]] Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez 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 microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total). Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois 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 microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement. Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations. ===La micro-fusion des processeurs Intel et AMD=== La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''. Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''. Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout. La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Le parallélisme mémoire | prevText=Le parallélisme mémoire | next=Exemples de microarchitectures CPU : le cas du x86 | nextText=Exemples de microarchitectures CPU : le cas du x86 }} </noinclude> mgg1mozbl6wcf71lv4g5gr1xb9gx6ur 772800 772799 2026-09-22T00:06:10Z Mewtow 31375 /* L'émission multiple des accès mémoire avec une LSQ */ 772800 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. 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, exu 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 possibels 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'atterir 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|Emission multiple des opérations entières, implémentation courante]] Pour les branchements, deux solutions sont possibles. Avec la première, il y a émission parallèle des branchements, à savoir qu'il est possible d'émettre en branchement en parallèle de deux-trois instructions entières. 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|Emission 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 microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle. 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 simultanné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|Emission 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 microarchitecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet d'émettre deux lectures en même temps qu'une écriture. [[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]] Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettent plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail. Pour rappel, l'unité mémoire est précédée par une structure 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 particlier 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:Emission LOAD - Store séparée.png|thumb|Emission LOAD - Store séparée]] ==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 autoaligné.]] Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements ! [[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]] Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez 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 microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total). Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois 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 microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement. Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations. ===La micro-fusion des processeurs Intel et AMD=== La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''. Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''. Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout. La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Le parallélisme mémoire | prevText=Le parallélisme mémoire | next=Exemples de microarchitectures CPU : le cas du x86 | nextText=Exemples de microarchitectures CPU : le cas du x86 }} </noinclude> lqpt195ryi8dyc28rxvn0tldohp8mbo 772801 772800 2026-09-22T00:14:13Z Mewtow 31375 /* L'émission séparée des LOAD et STORE */ 772801 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. 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, exu 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 possibels 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'atterir 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|Emission multiple des opérations entières, implémentation courante]] Pour les branchements, deux solutions sont possibles. Avec la première, il y a émission parallèle des branchements, à savoir qu'il est possible d'émettre en branchement en parallèle de deux-trois instructions entières. 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|Emission 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 microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle. 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 simultanné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|Emission 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 microarchitecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet 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. 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|Emission LOAD - Store séparée]] Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettent plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail. Pour rappel, l'unité mémoire est précédée par une structure 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 particlier 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 autoaligné.]] Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements ! [[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]] Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez 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 microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total). Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois 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 microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement. Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations. ===La micro-fusion des processeurs Intel et AMD=== La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''. Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''. Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout. La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Le parallélisme mémoire | prevText=Le parallélisme mémoire | next=Exemples de microarchitectures CPU : le cas du x86 | nextText=Exemples de microarchitectures CPU : le cas du x86 }} </noinclude> h7wq6wiy9z9wltvhy5rz3not9ee86lt 772802 772801 2026-09-22T00:21:34Z Mewtow 31375 /* L'émission séparée des LOAD et STORE */ 772802 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. 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, exu 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 possibels 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'atterir 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|Emission multiple des opérations entières, implémentation courante]] Pour les branchements, deux solutions sont possibles. Avec la première, il y a émission parallèle des branchements, à savoir qu'il est possible d'émettre en branchement en parallèle de deux-trois instructions entières. 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|Emission 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 microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle. 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 simultanné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|Emission 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 microarchitecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet 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 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 directement à l'émission, pour qu'elles se fassent un peu en avance. 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|Emission 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 particlier 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]] Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettent plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail. ==Les optimisations des processeurs superscalaires larges== Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul. ===L'étape de chargement d'un CPU superscalaire large=== Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 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 autoaligné.]] Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements ! [[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]] Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez 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 microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total). Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois 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 microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement. Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations. ===La micro-fusion des processeurs Intel et AMD=== La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''. Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''. Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout. La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Le parallélisme mémoire | prevText=Le parallélisme mémoire | next=Exemples de microarchitectures CPU : le cas du x86 | nextText=Exemples de microarchitectures CPU : le cas du x86 }} </noinclude> ip1tygca2kwl45qwrb3dwmw0lgpioqt 772803 772802 2026-09-22T00:23:46Z Mewtow 31375 /* L'émission séparée des LOAD et STORE */ 772803 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. 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, exu 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 possibels 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'atterir 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|Emission multiple des opérations entières, implémentation courante]] Pour les branchements, deux solutions sont possibles. Avec la première, il y a émission parallèle des branchements, à savoir qu'il est possible d'émettre en branchement en parallèle de deux-trois instructions entières. 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|Emission 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 microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle. 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 simultanné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|Emission 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 microarchitecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet 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|Emission 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 particlier 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]] Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettent plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail. ==Les optimisations des processeurs superscalaires larges== Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul. ===L'étape de chargement d'un CPU superscalaire large=== Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 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 autoaligné.]] Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements ! [[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]] Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez 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 microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total). Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois 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 microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement. Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations. ===La micro-fusion des processeurs Intel et AMD=== La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''. Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''. Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout. La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Le parallélisme mémoire | prevText=Le parallélisme mémoire | next=Exemples de microarchitectures CPU : le cas du x86 | nextText=Exemples de microarchitectures CPU : le cas du x86 }} </noinclude> 6d1yqyb77rduayz0p1o00x8dg01uoxm 772804 772803 2026-09-22T00:26:44Z Mewtow 31375 /* L'émission séparée des LOAD et STORE */ 772804 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. 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, exu 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 possibels 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'atterir 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|Emission multiple des opérations entières, implémentation courante]] Pour les branchements, deux solutions sont possibles. Avec la première, il y a émission parallèle des branchements, à savoir qu'il est possible d'émettre en branchement en parallèle de deux-trois instructions entières. 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|Emission 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 microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle. 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 simultanné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|Emission 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 microarchitecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet 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|Emission 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 particlier 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 autoaligné.]] Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements ! [[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]] Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez 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 microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total). Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois 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 microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement. Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations. ===La micro-fusion des processeurs Intel et AMD=== La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''. Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''. Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout. La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Le parallélisme mémoire | prevText=Le parallélisme mémoire | next=Exemples de microarchitectures CPU : le cas du x86 | nextText=Exemples de microarchitectures CPU : le cas du x86 }} </noinclude> 44n645v3g85zql363cx7wp98nq0r6o4 772806 772804 2026-09-22T07:07:40Z DavidL 1746 772806 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. 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|Emission 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> 0msiv8939r8jmtinnsebz620qakmh2t 772807 772806 2026-09-22T07:08:14Z DavidL 1746 /* L'émission multiple des micro-opérations entières */ 772807 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. 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> dzje28c2nlu2iw0nrzap0jqy8frg53y Fonctionnement d'un ordinateur/Les optimisations du chargement des instructions 0 79799 772768 747306 2026-09-21T22:21:20Z Mewtow 31375 /* Le préchargement d'instructions et la Fetch Target Queue */ 772768 wikitext text/x-wiki Les processeurs avec un pipeline sont découpés en deux sections : un ''front-end'' qui charge et décode les instructions, et un ou plusieurs ''back-end'' qui exécutent les instructions. L'exécution des micro-opérations est optimisée par des techniques que nous verrons dans la suite du cours : exécution dans le désordre, superscalarité, renommage de registre. Mais un point important est que le ''front-end'' a des optimisations dédiées. La prédiction de branchement est l'une de ces optimisation, mais elle n'est pas la seule. Ce chapitre aborde les optimisations autres que la prédiction de branchement. Nous allons voir le découplage des étages du ''front-end'', le pré-décodage et quelques améliorations liées indirectement à la prédiction de branchement. ==Le découplage du ''front-end''== Le séquenceur d'un processeur contient une unité de chargement, un décodeur et un chemin de données. Avec un pipeline, le couplage de ces structures fait que si l'une d'entre elle prend plus d'un cycle pour faire son travail, les étages précédents et/ou suivants sont stoppés. Par exemple, lors d'un défaut dans le cache d'instruction, l'ensemble stoppe. De même, si jamais le décodeur doit décoder une instruction via le micro-code, cela prend plusieurs cycles : l'unité de chargement et le cache d'instruction sont inutilisés. Même chose si jamais une instruction multicycle s’exécute dans le pipeline : cela bloque toutes les étapes précédentes. Mais il s'agit là d'un défaut inhérent aux pipelines basiques, qui relient chaque étage avec des registres. Avec un peu d'astuce, il est possible que certains étages prennent de l'avance même si l'étage suivant est bloqué. Par exemple, si le décodeur bloque le pipeline en utilisant son micro-code pendant 4-5 cycles, l'unité de chargement peut en théorie précharger à l'avance les instructions suivantes, et les mettre en attente. Ou encore, en cas de bulle de pipeline (''pipeline stall''), le décodeur peut décoder en avance des instructions et les mettre en attente tant que la bulle de pipeline est en cours. Dans les deux cas, une unité prend de l'avance et met en attente ses résultats tant que l'étage suivant est occupé. La mise en attente est réalisée en remplaçant/complémentant les registres du pipeline avec des mémoires FIFOs. Elles remplacent ou complémentant les registres de pipeline, les deux sont possibles. Si elles remplacent ces registres, si aucune mise en attente n'est requise, elles fonctionnent comme un registre de pipeline. ===La file d'instruction=== [[File:File de micro-opérations.png|vignette|upright=1|File d'instructions]] Les processeurs modernes intègrent une '''file d'instruction''', à savoir une mémoire FIFO placée entre le cache d'instruction et le décodeur d'instruction. Les instructions chargées par l'étape de chargement soient accumulées dans la file d'instructions et sont décodées quand l'unité de décodage est prête. La file d'attente permet de charger des instructions à l'avance, permettant ainsi de masquer certains accès au cache ou à la mémoire assez longs. L'idée est que les instructions s'accumulent dans la file d'instruction si le processeur exécute les instructions moins vite qu'il ne les charge. C'est généralement signe qu'il effectue une instruction multicycle et/ou qu'il effectue un accès à la mémoire. À l'inverse, la file d'attente se vide quand le processeur exécute les instructions plus vite qu'il n'en charge. C'est généralement signe qu'un défaut de cache d'instruction est en cours. La présence d'une file d'attente fait que la première situation est compensée lors de la seconde. Les temps d'attentes liées aux instructions multicycles permettent de remplir la file d'attente, qui est ensuite vidée en cas de défaut de cache. Le processeur exécute en permanence des instructions, sans interruption. Alors que sans file d'attente, les défauts de cache entraineront des temps d'attente où le processeur s’exécuterait rien. La seule limite de cette optimisation est l'influence des branchements. Lorsqu'un branchement est décodé, ce tampon d’instructions est totalement vidé de son contenu. Ce n'est ni plus ni moins ce que faisait la ''prefetch input queue'' des anciens processeurs Intel, dont nous avions parlé dans le chapitre sur l'unité de chargement et le séquenceur. ===La file de micro-opérations=== [[File:File de micro-opération.png|vignette|upright=1|File d'instruction]] L'optimisation précédente peut aussi s'appliquer entre le décodeur d'instruction et le chemin de données. Pour cela, la sortie du décodeur est reliée à une mémoire FIFO semblable à la file d'instruction. Elle mémorise les micro-opérations émises par le décodeur et les met en attente tant que le reste du pipeline n'est pas prêt. Nous l'appellerons la '''file de micro-opérations''', mais elle porte de nombreux noms et la terminologie des différents fabricants est assez confuse. Le schéma ci-contre indique que la file de micro-opérations est située en sortie de l’unité de décodage, avant l'unité d'émission et avant l'unité de renommage de registres (que nous aborderons dans quelques chapitres). La file de micro-opérations permet aux décodeurs de faire leur travail même si le reste du pipeline n'est pas prêt. Par exemple, imaginons que le processeur ne peut pas émettre de nouvelle instruction, soit car toutes les ALUs sont occupées, soit car il y a un accès mémoire qui bloque le pipeline, peu importe. Sans file de micro-opérations, tout ce qui précède l'unité d'émission devrait être totalement bloqué tant que l'instruction ne peut pas être émise. Mais avec une file de micro-opérations, le pipeline peut charger et décoder des instructions, il a juste à accumuler les instructions décodées dans la file de micro-opérations. En clair, la file de micro-opérations met en attente les instructions décodées quand des bulles de pipeline sont émises. Et à l'inverse, elle permet d'émettre des instructions quand les unités de décodage/chargement sont bloquées. Le cas classique est celui d'un défaut de cache dans le cache d'instruction. Pendant un défaut de cache, aucune instruction n'est chargée ni décodées durant quelques cycles. Sans file de micro-opérations, le processeur ne peut plus rien faire durant quelques cycles. Mais avec une file de micro-opérations, il se rattrape en émettant les instructions en attente dans la file de micro-opérations, s'il y en a. En clair, si l'unité d'émission a mis en attente des instructions, le processeur se rattrape au prochain défaut de cache d'instruction. Une autre situation où le décodeur bloque est le cas où certaines instructions mettent du temps à être décodées. C'est notamment le cas de certaines instructions complexes, dont le décodage prend facilement 2 à 3 cycles d'horloge, voire plus. Le pire est le décodage des instructions microcodées, qui peut demander plusieurs cycles. Or, le pipeline demande qu'on décode une instruction par cycle pour éviter de bloquer le pipeline. Mais ce temps de décodage peut être masqué si des micro-opérations sont en attente dans la file, elles sont exécutées pendant le décodage long. ===Le ''Loop Stream Detector''=== Les boucles sont une opportunité d'optimisation très intéressante sur les CPU avec une file de micro-opérations. L'idée est que lors d'une boucle, des instructions sont chargées, décodées et exécutées plusieurs fois de suite. Mais à chaque répétition d'une instruction, le chargement et le décodage donnent toujours le même résultat, seule l'exécution n'est pas la même (les registres renommés sont aussi différents, mais nous verrons cela dans plusieurs chapitres). L'idée est simplement de mémoriser les N dernières instructions décodées et de les ré-exécuter si besoin. Ainsi, on évite de charger/décoder une même instruction machine plusieurs fois, mais de réutiliser les micro-opérations déjà décodées. L'implémentation la plus simple utiliser la file de micro-opérations comme une sorte de pseudo-cache FIFO. La file de micro-opérations ne supprime pas les micro-opérations une fois qu'elles sont émises. Elle mémorise là où se trouve la dernière micro-opération émise, mais conserve celles qui ont déjà été émises. Un circuit annexe, appelé le '''''Loop Stream Detector''''' (LSD), détecte les boucles dans la file de micro-opérations et optimise leur exécution. Si une boucle adéquate est détectée par le ''Loop Stream Detector'', les micro-opérations de la boucle sont lues dans la file de micro-opération et sont injectées directement dans la suite du pipeline. De plus, les unités de chargement et de décodage sont désactivées pendant l’exécution de la boucle, ce qui réduit la consommation d'énergie du CPU. L'optimisation accélère les petites boucles, qui tiennent toutes entières dans la file de micro-opérations, et sous condition qu'elles s'exécutent de la même manière à chaque exécution. De telles boucles exécutent une suite de N instructions, qui reste identique à chaque itération de la boucle. Le cas le plus simple est celui d'une boucle dans laquelle il n'y a pas de branchements. Pour les boucles normales, le processeur reprend une exécution normale quand on quitte la boucle ou quand son exécution change, par exemple quand un if...else, un return ou tout autre changement de flot de contrôle a lieu. Vu que toutes ces situations impliquent un branchement qui n'a pas été pris comme avant, le processeur n'utilise plus le ''Loop Stream Detector'' en cas de mauvaise prédiction de branchement. Le LSD vise surtout à désactiver les décodeurs et l'unité de chargement lors de l'exécution d'une boucle. La désactivation peut être du ''clock gating'', voire du ''power gating'', être partielle ou totale. Dans le pire des cas, les unités de chargement peuvent continuer à charger des instructions en avance dans une file d'instruction, mais les décodeurs peuvent être désactivés. Dans le meilleur des cas, la totalité de ce qui précède la file de micro-opération est désactivé tant que la boucle s’exécute normalement. Y compris le cache de micro-opération. [[File:Loop Stream Detector.png|centre|vignette|upright=2|Loop Stream Detector]] Évidemment, la taille des boucles optimisées ainsi est limitée par la taille de la file de micro-opération, ce qui fait que l'optimisation ne fonctionne que pour des boucles de petite taille. Pour donner quelques chiffres, les processeurs ARM Cortex A15 géraient des boucles de maximum 32 micro-opérations. De plus, toute la file de micro-opération n'est pas gérée par le ''loop stream detector''. Par exemple, les processeurs avec une file de micro-opération de 64 micro-opération peuvent gérer des boucles de maximum 32 à 40 micro-opérations. Mais les contraintes principales portent sur la détection des boucles. Le ''Loop Stream Detector'' ne peut pas détecter toutes les boucles qui existent, et certaines boucles ne sont pas détectées. Par exemple, le ''Loop Stream Detector' ne peut pas détecter les boucles si un appel de fonction a lieu dans la boucle. Il y a aussi des contraintes quant au nombre de branchements à l'intérieur de la boucle et le nombre d'accès mémoire. Les CPU Intel modernes disposent d'un ''loop stream detector'', les CPU AMD en avaient sur les microarchitectures Zen 4 mais il a disparu sur la microarchitecture Zen 5. Quelques CPU ARM avaient aussi un ''loop stream detector'', notamment le Cortex A15. Il faut noter que le ''loop stream detector'' a été désactivé par des mises à jour de microcode sur quelques architectures, comme sur la microarchitecture Zen 4 d'AMD ou les CPU de microarchitecture Skylake et Kaby Lake d'Intel. Pour la microarchitecture Skylake, les raisons officielles pour cette désactivation sont un bug lié à l'interaction avec l'''hyperthreading''. Il est vraisemblable que des bugs ou des problèmes de sécurité aient amené à la désactivation sur les autres architectures. ===Le cache de micro-opérations=== Le '''cache de micro-opérations''' a le même but que le ''Loop Stream Detector'', à savoir optimiser l'exécution des boucles. La différence avec le ''Loop Stream Detector'' est qu'il y a un cache séparé de la file de micro-opérations, qui mémorise des micro-opérations décodées, dans le cas où elles soient réutilisées par la suite. La première itération d'une boucle accumule les instructions décodées dans le cache de micro-opérations, les itérations suivantes de la boucle lisent les micro-opérations adéquates dans le cache de micro-opération : on n'a pas à décoder l'instruction une nouvelle fois. Sur de nombreux processeurs, le cache de micro-opération est alimenté non pas par l'unité de décodage, mais par la file de micro-opérations. Ainsi, seules les micro-opérations émises sont copiées dans ce cache. [[File:File de micro-opérations et cache de micro-ops.png|centre|vignette|upright=2|File de micro-opérations et cache de micro-ops]] Les avantages sont les mêmes qu'avec un ''Loop Stream Detector'' : une consommation énergétique réduite, des performances légèrement améliorées. Le décodeur et l'unité de chargement sont inutiles en cas de succès dans le cache de micro-opération, ce qui fait qu'ils sont désactivés, éteints, ou du moins subissent un ''clock-gating'' temporaire. Ils ne consomment pas d'énergie, seul le cache de micro-opération utilise de l'électricité. L'avantage en termes de performance est plus faible, assez variable suivant la situation, mais aussi bien le cache de micro-opérations que le LSD ne font pas de mal. Une différence avec le cache de micro-opération est qu'une boucle doit s’exécuter à l'identique avec un ''Loop Stream Detector'', pas avec un cache de micro-opérations. Prenons l'exemple d'une boucle contenant quelques instructions suivies par un IF...ELSE. Il arrive qu'une itération de la boucle exécute le IF, alors que d'autres exécutent le ELSE. Dans ce cas, le ''Loop Stream Detector'' ne sera pas activé, car la boucle ne s’exécute pas pareil d'une itération à l'autre. Par contre, un cache de macro/micro-opération mémorisera les micro-opérations du IF et du ELSE et les fournira selon les besoins. La raison est que le cache de micro-opérations a une politique de remplacement des lignes de cache plus complexe que le FIFO, typiquement une politique LRU ou LFU approximée. Le cache de micro-opération est donc plus efficace que le ''Loop Stream Detector'', pour un cout en transistor plus élevé. Le cache de micro-opération est une voie de chargement parallèle au ''front-end'' proprement dit. L'accès au cache de micro-opération se fait lors de l'étape de chargement. Le cache de micro-opérations est adressé en envoyant le ''program counter'' sur son entrée d'adresse, en parallèle du cache d'instruction. En clair, il y a une voie qui regroupe cache d'instruction, file d'instruction et décodeur, et une seconde voie qui se résume au cache de micro-opération. Les deux voies sont accédées en parallèle. En cas de succès dans le cache de micro-opération, les micro-opérations adéquates sont lues directement depuis le cache de micro-opération. Le cache de micro-opération associe, pour chaque instruction machine, une ou plusieurs micro-opérations. Avec l'implémentation la plus simple, une ligne de cache est associée à une instruction machine. Par exemple, sur les processeurs Intel de microarchitecture Skylake, chaque ligne de cache était associée à une instruction machine et pouvait contenir de 1 à 6 micro-opérations. Une instruction devait tenir toute entière dans une ligne de cache, ce qui fait que les instructions décodées en plus de 6 micro-opérations ne pouvaient pas rentrer dans ce cache. Il existe deux méthodes différentes pour encoder les micro-opérations dans le cache de micro-opérations. La première est la plus intuitive : on mémorise les micro-opérations dans la ligne de cache, directement. Elle est utilisée sur les processeurs AMD, et sans doute sur les processeurs Intel récents. Mais les anciens processeurs Intel, comme ceux des architectures Sandy Bridge et Netburst, utilisent une autre méthode. Une ligne de cache mémorise non pas les micro-opération directement, mais un pointeur vers le ''control store'', qui indique à quelle adresse dans le micro-code se situe la micro-opération. La micro-opération est donc lue depuis le micro-code lors de son envoi au chemin de données, lors de son émission. Il faut noter que pour des raisons de performance, le cache de micro-opérations est virtuellement tagué, ce qui fait qu'il est invalidé en cas de changement de programme. Sur l'architecture Sandy Bridge, il est carrément inclus dans le cache L1, les deux sont des caches inclusifs l'un avec l'autre. Les premières implémentations étaient très limitées. Les micro-opérations devaient être séquentielles dans le code, le cache était consulté seulement après un branchement et non à chaque instruction, pour limiter la consommation d'énergie an détriment des performances. Ces limitations ne sont pas présentes sur les architectures récentes. Le cache de micro-opérations et le ''Loop Stream Detector'' font la même chose, mais certains processeurs implémentaient les deux. L'avantage est que le cache de micro-opération peut être désactivé si jamais le LSD détecte une boucle dans la file d'instruction, ce qui réduit encore plus la consommation énergétique. En pratique, l'impact sur la consommation énergétique est très difficile à mesurer, mais il rajoute de la complexité pour la conception du processeur. [[File:File de micro-opérations et cache de micro-ops - Copie.png|centre|vignette|upright=2.5|File de micro-opérations et cache de micro-ops - Copie]] ==Le ''Branch Folding'' des CPU PowerPC== Le '''''Branch Folding''''' est une optimisation qui permet d'exécuter les branchements en avance, pendant qu'ils sont encore dans la file d'instruction. Pour cela, une unité spécialisée scanne la file d'instruction pour y trouver les branchements. Quand elle tombe sur un branchement, elle l'exécute en avance et altère le contenu de la file d'instruction pour remplacer les instructions chargées à tord. L'unité en question s'appelle l''''unité de branchements anticipés'''. Pour les branchements inconditionnels, elle peut les "exécuter directement". Elle retire le branchement et le remplace par l'instruction de destination du branchement. Le ''program counter'' est aussi altéré en avance, ce qui fait que les instructions adéquates sont ensuite chargées dans la file d'instruction. Pour les branchements conditionnels, elle se base sur les résultats de la prédiction de branchement pour savoir s'ils sont pris ou non. Si le branchement est considéré comme pris, il est traité comme un branchement inconditionnel : il est remplacé par l'instruction de destination et le ''program counter'' est altéré. SI le branchement est non-pris, il suffit de ne rien faire. Les premiers processeurs PowerPC utilisaient cette optimisation. Ils avaient une file d'instruction de 8 instructions, et l'unité de branchement scannait les 4 premières instructions. L'unité de branchements anticipés contenait les registres nécessaires pour gérer les branchements. Elle contenait précissément le ''link register'' pour des appels de procédures, le ''Count Target Register'' et le ''Count Register'' utilisés pour les boucles. Il contient aussi un additionneur pour calculer les adresses des branchements relatifs. ==Le préchargement d'instructions et la ''Fetch Target Queue''== Nous venons de voir qu'il est possible de découpler les étages de chargement, décodage et le chemin de données, en insérant des mémoires FIFOs dans le pipeline. Il en est de même avec l'unité de chargement elle-même. Elle est composée de deux circuits entre lesquels on peut ajouter des mémoires FIFO : une unité de calcul d'adresse et le cache d'instruction. L'unité de calcul d'adresse regroupe : l'unité de prédiction de branchement, le ''program counter'', le circuit pour incrémenter le ''program counter'', les MUX associés pour gérer les branchements. L'unité de calcul d'adresse émet les adresses des instructions à charger, qui sont consommées par le cache d'instruction. Les processeurs modernes incorporent une optimisation assez intéressante : ils découplent l'unité de calcul d'adresse de l'accès au cache d'instruction. Pour cela, ils incorporent une mémoire FIFO entre l'unité de prédiction de branchement et le cache d'instruction. Les premiers articles scientifiques, qui ont proposé cette solution, l'ont appelée la '''''Fetch Target Queue''''', abréviée FTQ. Elle accumule les adresses à lire/écrire dans le cache d'instruction, peu importe que ces adresses viennent du ''program counter'' ou de l'unité de prédiction de branchement. [[File:Fetch target queue.png|centre|vignette|upright=2.5|Fetch target queue]] Elle se remplit quand le cache d'instruction est bloqué, soit à cause d'un défaut de cache, soit à cause d'un pipeline bloqué en amont de l'unité de chargement. Par exemple, si le cache d'instruction est bloqué par un défaut de cache, l'unité de prédiction de branchement peut accumuler des prédictions à l'avance dans la FTQ, qui sont ensuite consommées par le cache d'instruction une fois qu'il est redevenu disponible. De même, si l'unité de prédiction de branchement est bloquée par un évènement quelconque, le cache d'instruction peut consommer les prédictions faites à l'avance. Une utilisation assez originale de la FTQ s'est vu sur les processeurs AMD d'architectures bulldozer. Sur cette architecture, les cœurs étaient regroupés par paquets de deux, et les deux cœurs partageaient certains circuits. Notamment, l'unité de prédiction de branchement était partagée entre les deux cœurs ! Pourtant, chaque cœur disposait de sa propre FTQ ! Un avantage de la FTQ tient dans le fait que les caches d'instructions sont pipelinés, sur le même modèle que les processeurs. On peut leur envoyer une demande de lecture/écriture par cycle, alors que chaque lecture/écriture prendra plusieurs cycles à s'effectuer. L'accès au cache d'instruction a donc une certaine latence, qui est partiellement masquée par la FTQ au point où elle ne s'exprime qu'en cas de défaut de cache assez important. Par exemple, si l'accès au cache d'instruction prend 4 cycles, une FTQ qui met en attente 4 adresses camouflera le temps d'accès au cache, tant qu'il n'y a pas de mauvaise prédiction de branchement. La FTQ est aussi très utile avec les unités de branchement modernes, qui peuvent mettre plusieurs cycles pour fournir une prédiction. Prendre de l'avance avec une FTQ amorti partiellement le temps de calcul des prédictions. : Si le cache d'instruction est multiport et accepte plusieurs accès simultanés, il peut consommer plusieurs entrées dans la FTQ à la fois. Mais l'avantage principal de la FTQ est qu'elle permet l'implémentation d'une optimisation très importante. Il y a quelques chapitres, nous avions parlé des techniques de '''préchargement d'instruction''', qui permettent de charger à l'avance des instructions dans le cache d'instruction. Nous avions volontairement laissé de côté le préchargement des instructions, pour tout un tas de raisons. Et la raison est justement que la prédiction de branchement et le préchargement des instructions sont fortement liés sur les processeurs modernes. Il est maintenant possible d'aborder le préchargement pour les instructions, d’où cette section. Notons que par préchargement des instructions, on peut parler de deux formes de préchargement, fortement différentes. La première correspond au préchargement normal, à savoir le préchargement des instructions dans le cache d'instruction L1, à partir du cache L2. Il s'agit donc d'un préchargement dans le cache d'instruction. Mais il existe aussi une autre forme de préchargement, qui consiste à précharger à l'avance des instructions dans la file d'instruction et qui a été abordée dans la section sur la ''prefetch input queue''. Les deux formes de préchargement n'ont pas lieu au même endroit dans la hiérarchie mémoire : l'une précharge du cache L2 vers le L1i, l'autre du cache L1i vers la file d'instruction (ou dans le cache de macro-opération). Mais les algorithmes utilisés pour sont sensiblement les mêmes. Aussi, nous allons les voir en même temps. Pour faire la distinction, nous parlerons de préchargement L2-L1i pour la première, de préchargement interne pour l'autre. ===Les algorithmes de préchargement d'instructions=== Les techniques basiques de préchargement consistent à charger des instructions qui suivent la dernière ligne de cache accédée. Quand on charge des instructions dans le cache d’instruction, les instructions qui suivent sont chargées automatiquement, ligne de cache par ligne de cache. il s'agit due préchargement séquentiel, la technique la plus simple de préchargement, qui profite de la localité spatiale. Elle est utilisée pour précharger des instructions du cache L2 vers le cache L1i, mais aussi pour le préchargement interne dans la file d'instructions. [[File:Branchements et préchargement séquentiel.png|centre|vignette|upright=2|Branchements et préchargement séquentiel.]] Mais un ''prefetcher'' purement séquentiel gère mal les branchements. Si un branchement est pris, les instructions de destination ne sont pas chargées, si elles ne sont pas dans la ligne de cache suivante. Pour le préchargement L2-L1i, cela ne pose pas de problèmes majeurs, au-delà de la pollution du cache L1i par des instructions inutiles. Mais pour le préchargement interne, c'est autre chose. Les instructions préchargées par erreurs doivent être supprimées pour éviter qu'elles soient décodées et exécutées, ce qui fait que la file d’instruction doit être invalidée. Il existe des techniques de préchargement plus élaborées qui marchent mieux en présence de branchements. Elles utilisent toutes une collaboration de l'unité de prédiction de branchement. Elles accèdent au ''Branch Target Buffer'', pour détecter les branchements, leur destination, etc. Le tout peut se coupler à la technique du prédécodage. Avec cette dernière, le prédécodage décode en partie les instructions lors de leur chargement dans le cache, et détecte les branchements et leur adresse de destination à ce moment-là. Ces informations sont alors mémorisées dans une table à part, ou dans le BTB. Mais la plupart des designs utilisent le BTB, par souci de simplicité. Il existe globalement deux à trois techniques principales, que nous allons voir dans ce qui suit. La première technique prédit si le branchement est pris ou non, et agit différemment si le branchement est pris ou non. Si le branchement est pris, elle précharge les instructions à partir de l'adresse de destination des branchements pris. Sinon, elle précharge les instructions suivantes avec préchargement séquentiel. Il s'agit du '''''target line prefetching''''' [[File:Target line prefetching.png|centre|vignette|upright=2|Target line prefetching.]] Une autre technique ne prédit pas les branchements et précharge à la fois les instructions suivantes avec le ''next-line prefetching'', et la ligne de cache de destination du branchement avec le ''target line prefetching''. Comme ça, peu importe que le branchement soit pris ou non, les instructions adéquates seront préchargées quand même. On appelle cette technique le '''préchargement du mauvais chemin''' (''wrong path prefetching''). [[File:Préchargement du mauvais chemin.png|centre|vignette|upright=2|Préchargement du mauvais chemin.]] Le ''target line prefetching'' est plus complexe à implémenter, car il demande de prédire les branchements. Mais elle a l'avantage de ne pas précharger inutilement deux lignes de cache par branchement, seulement une seule. Par contre, le préchargement est inutile en cas de mauvaise prédiction de branchement : non seulement on a préchargé une ligne de cache inutilement, mais en plus, la ligne de cache adéquate n'a pas été chargée. On n'a pas ce problème avec le préchargement du mauvais chemin, qui garantit que la ligne de cache adéquate est toujours préchargée. ===L'implémentation du préchargement interne, dans la file d'instruction=== Le préchargement dans la file d'instruction est généralement de type séquentiel, mais certains processeurs font autrement. Déjà, il faut remarquer que le ''target line prefetching'' correspond en réalité à la prédiction de branchement classique. L'adresse de destination est prédite, et on charge les instructions adéquates dans la file d'instruction. La prédiction de branchement, associée à une file d'instruction, est donc une forme de préchargement. Il fallait y penser. Enfin, des processeurs assez rares utilisaient le préchargement du mauvais chemin. Le préchargement du mauvais chemin demande d'utiliser deux files d'instructions séparées. L'une dans laquelle on précharge de manière séquentielle, l'autre dans laquelle on utilise la prédiction de branchement pour faire du ''target line prefetching''. Une fois que l'on sait si la prédiction de branchement était correcte, on est certain qu'une des deux files contiendra les instructions valides. Le contenu de la file adéquate est conservé, alors que l'autre est intégralement invalidée. Le choix de la bonne file se fait avec un multiplexeur. C'est approximativement la technique qui était implémentée sur le processeur de mainframe IBM 370/165, par exemple, et sur quelques modèles IBM similaires. Le problème est que cette méthode demande de charger deux instructions à chaque cycle. Cela demande donc d'utiliser un cache d'instruction multiport, avec un port par file d'instruction. Le cout en circuit d'un cache double port n'est pas négligeable. Et le gain en performance est assez faible. Le préchargement dans la file d’instruction permet d'économiser quelques cycles lors de l'accès au cache d'instruction, guère plus. Le gain est maximal lorsque les instructions préchargées ont généré un défaut de cache, qui a rapatrié les instructions adéquates pendant que le processeur exécutait les mauvaises instructions, avant que la mauvaise prédiction de branchement soit détectée. Dans ce cas, le défaut de cache a eu lieu pendant la mauvaise prédiction et sa réparation, et non après. ====La gestion des branchements successifs==== Un autre défaut de cette méthode est la présence de branchements successifs. Par exemple, si jamais on rencontre un branchement, le flux d'instructions se scinde en deux : un où le branchement est pris, un autre où il ne l'est pas. Chacun de ces flux peut lui-même contenir un branchement, et se scinder lui aussi. Et ainsi de suite. Et le processeur doit gérer cette situation en termes de préchargement. [[File:Exécution stricte 04.png|centre|vignette|upright=2|Exécution stricte]] Plusieurs solutions existent. La méthode la plus simple stoppe le chargement du flux en attendant que le premier branchement soit terminé. Cette solution est intuitive, mais est celle où on a les gains en performance les plus faibles. Elle est couramment implémentée d'une manière assez particulière, qui ne correspond pas tout à fait à un stop du chargement, mais qui utilise les lignes de cache. L'unité de préchargement est conçue pour copier des lignes de cache entières dans la file d'instruction. Le processeur (pré-)charge deux lignes de cache : celle du bon chemin, celle du mauvais chemin. Il les précharge dans deux files d'instructions, qui contiennent généralement une ligne de cache grand maximum. Le temps que l'on ait chargé les deux files d'instruction, le résultat du branchement est connu et on sait laquelle est la bonne. L'autre possibilité est d'utiliser la prédiction de branchement pour ce flux, afin de poursuivre le chargement de manière spéculative. Elle donne de bonnes performances, mais demande des unités de prédiction de branchement spéciales, dans le cas où les deux flux tombent sur un branchement en même temps. Cette technique est indirectement liée au cache de traces que nous verrons dans le chapitre sur les processeurs superscalaires. Nous n'en parlons pas ici, car ce genre de techniques est plus liée aux processeurs superscalaires qu'un processeur avec un pipeline normal. Une autre possibilité consiste à scinder ce flux en deux et charger les deux sous-flux. Cette dernière est impraticable car elle demande des caches avec un grand nombre de ports et la présence de plusieurs files d'instructions, qui sont utilisées assez rarement. [[File:Exécution stricte 01.png|centre|vignette|upright=2|Exécution stricte, seconde.]] ====Les processeurs à exécution de chemins multiples==== L'idée précédente peut en théorie être améliorée, afin de non seulement charger les instructions en provenance des deux chemins (celui du branchement pris, et celui du branchement non pris), mais aussi de les exécuter : c'est ce qu'on appelle l''''exécution stricte''' (''eager execution''). Bien sûr, on n’est pas limité à un seul branchement, mais on peut poursuivre un peu plus loin. Quelques papiers de recherche ont étudié l'idée, mais ses défauts font qu'elle n'a jamais été utilisée dans un processeur en dehors de prototypes destinés à la recherche. Le gros problème de l'exécution stricte est qu'on est limité par le nombre d'unités de calculs, de registres, etc. Autant ce serait une technique idéale sur des processeurs avec un nombre illimité de registres ou d'unités de calcul, autant ce n'est pas le cas dans le monde réel. Au bout d'un certain nombre d’embranchements, le processeur finit par ne plus pouvoir poursuivre l’exécution, par manque de ressources matérielles et doit soit stopper, soit recourir à la prédiction de branchement. Il y a le même problème avec le préchargement interne simple, quand on utilise le préchargement du mauvais chemin, comme vu juste au-dessus. ===L'implémentation matérielle du préchargement de cache L2-L1i=== Pour comprendre comment s'effectue le préchargement L2-L1i, il faut regarder comment l'unité de chargement communique avec les caches. L'unité de prédiction de branchement est généralement regroupée avec le ''program counter'' et les circuits associés (les incrémenteurs/MUX associés), pour former l'unité de chargement proprement dite. L'unité de chargement émet des adresses consommées par le cache d'instruction, qui lui-même envoie les instructions lues dans le registre d'instruction ou la file d'instructions. Le couplage de ces structures fait qu'au moindre défaut de cache d'instruction, l'ensemble stoppe. Et notamment, l'unité de prédiction de branchement stoppe en cas de défaut de cache. Même chose si jamais une instruction multicycle s’exécute dans le pipeline et bloque toutes les étapes précédentes. Les pertes de performance ne sont pas très importantes, mais elles existent. Et le préchargement se manifeste dans ces situations. Le préchargement d'instructions consiste à découpler ces structures de manière à ce qu'elles fonctionnent plus ou moins indépendamment. Le but est qu'en plus des accès normaux au cache d'instruction, l'unité de chargement envoie des informations au cache L2 ou L1i en avance, pour effectuer le préchargement. L'unité de chargement doit alors prendre de l'avance sur le cache, pour effectuer les accès au cache L2 en avance, tout en maintenant l'état normal pour effectuer les accès normaux. C'est donc plus ou moins l'unité de chargement qui s'occupe du préchargement, ou du moins les deux sont très liées. ====L'anticipation du ''program counter''==== Avec la solution la plus simple, on a une unité de chargement qui s'occupe des accès au cache d'instruction, et une unité de préchargement qui prend de l'avance sur l'unité de chargement, et communique avec le cache L2. La technique la plus basique se base sur un ''Lookahead program counter'', un second ''program counter'' qui ne fonctionne que lors d'un défaut de cache d'instruction. Il est initialisé avec le ''program counter'' lors d'un défaut de cache, puis il est incrémenté à chaque cycle et les branchements sont prédits, ce qui fait qu'il est mis à jour comme si l’exécution du programme se poursuivait, alors que le reste du processeur est mis en attente. La technique initiale utilisait ce second ''program counter'' pour accéder à une table de prédiction, qui associe à chaque valeur du ''program counter'', l'adresse des données chargées par l'instruction associée. Les adresses fournies à chaque cycle par cette table sont alors envoyées aux unités de préchargement pour qu'elles fassent leur travail. La technique permettait donc de précharger des données en cas de défaut de cache, mais pas d'instructions. Il ne s'agissait pas d'une technique de préchargement des instructions, mais de préchargement de données. La technique a ensuite été adaptée pour le chargement des instructions par Chen, Lee et Mudge. Leur idée utilisait deux unités de prédiction de branchements : une couplée à l'unité de chargement, l'autre pour le préchargement. La première utilisait le ''program counter'' normal, l'autre se déclenchait en cas de défaut de cache et utilisait un ''lookahead program counter''. Les adresses générées par le ''lookahead program counter'' étaient envoyée au cache d'instruction, sur un port de lecture séparé. La ligne de cache lue était alors prédécodée pour détecter les branchements, qui étaient prédits, et rebelote. Il est possible d'adapter la méthode pour que les adresses soient accumulées dans une mémoire FIFO, et étaient consommée par le cache d'instruction L2 pour le préchargement si la ligne de cache associée n'était pas dans le cache d’instruction. Les techniques modernes n'utilisent plus de seconde unité de prédiction de branchement, mais conservent un ''lookahead program counter''. Par contre, le BTB dispose de plusieurs ports : un pour la prédiction de branchement normale, l'autre pour le préchargement. L'unité de préchargement et l'unité de chargement accèdent toutes deux au BTB quand elles ont besoin de faire leurs prédictions, en parallèle. Typiquement, le BTB est accédé à chaque cycle pour la prédiction de branchement, à un rythme plus faible pour le préchargement. ====Le ''Fetch Directed Instruction Prefetching''==== Les processeurs modernes semblent utiliser un algorithme connu sous le nom de '''''Fetch Directed Instruction Prefetching'''''. Il utilise les adresses contenues dans la FTQ pour précharger les instructions adéquates du cache L2 vers le cache L1 d'instruction (L1i). L'unité de préchargement est placée en aval de la FTQ, elle lit son contenu, détecte quelles adresses correspondent à des lignes de cache à précharger, et envoie celles-ci au cache L2. Le préchargement du L2 vers le L1i a lieu quand le cache L2 est inutilisé, ou du moins quand il peut accepter une nouvelle lecture (dans le cas d'un cache multiport et/ou pipeliné). [[File:Fetch directed instruction prefetching.png|centre|vignette|upright=2.5|Fetch directed instruction prefetching]] On peut améliorer légèrement le design précédent sur plusieurs points. Pour éviter de polluer le cache L1 avec des lignes de caches préchargées à tort, il est possible d'ajouter un équivalent des ''stream buffer'' vus dans le chapitre sur le préchargement. Il s'agit d'une autre mémoire FIFO qui mémorise les lignes de cache préchargées. Les lignes de cache préchargées ne sont pas placées dans le cache L1i, mais dans cette file d'attente. Lors d'un accès au L1i, la file d'attente est consultée en parallèle. Si l'instruction voulue est dans la file d'attente, elle est lue depuis la file, et la ligne de cache associée est copiée dans le cache L1i. Mais c'est là une possibilité facultative. Un autre point est que l'unité de préchargement doit attendre que le cache L2 puisse accepter une nouvelle lecture pour lancer le préchargement d'une autre ligne de cache. Pour corriger cela, on ajoute une file d'attente entre le cache L2 et l'unité de préchargement, qui est évidemment une mémoire FIFO. Son utilité dépend des temps de lectures du cache L2, ainsi que de la taille de la FTQ. Elle n'est pas toujours nécessaire, certains processeurs ont un cache L2 assez lent pour qu'on ne puisse précharger qu'une seule ligne de cache avant que la FTQ soit complétement vide. Ces deux optimisations sont facultatives, mais elles étaient présentes dans l'article originel qui a proposé la technique. L'unité de préchargement doit détecter quelles sont les adresses de la FTQ qui ne sont pas déjà chargées dans le L1i. En effet, il est inutile de précharger une ligne de cache si celle-ci est déjà dans le cache L1i. L'unité de préchargement doit donc filtrer au mieux les adresses de la FTQ en deux classes : celles qui correspondent à une ligne de cache déjà dans le L1i, celles qui doivent être préchargées. Pour cela, l'unité de préchargement utilise la technique dit du '''''Cache Probe Filtering'''''. L'idée part du principe que le cache d'instruction L1 est multiport. Les ports du cache d'instruction ne sont pas toujours utilisés en même temps et il arrive qu'il y ait un port de lecture de libre. Le CPF utilise alors ce port inutilisé pour vérifier si la prochaine ligne de cache à précharger est dans le cache ou non. Si c'est le cas, on aura un succès de cache : la ligne de cache est oubliée, elle ne sera pas préchargée. Si ce n'est pas le cas on aura un défaut de cache : la ligne sera préchargée. Notez que l'on a pas besoin de lire la ligne en question, juste de vérifier les tags du cache. Dans ce cas, on peut ajouter des signaux de commande spécifiques pour le CPF, qui font une demi-lecture, qui ne vérifie que les tags, mais ne lit pas la donnée. On peut par exemple ajouter un port spécifique pour le CPF, purement en lecture et qui ne permet que de vérifier les tags. Ce port en plus a un cout en circuits plus faible qu'un port de lecture normal, mais ce n'est pas gratuit du tout. ==Le prédécodage d'instructions== La présence d'un cache d'instruction permet l'implémentation de certaines optimisations, dont la plus connue est la technique dite du '''prédécodage'''. Avec elle, lorsque les instructions sont chargées dans le cache d'instruction, elles sont partiellement décodées, grâce à un circuit séparé de l'unité de décodage d'instruction. Le décodage de l'instruction proprement dit est plus court, car une partie du travail est faite en avance, on gagne quelques cycles. [[File:Prédécodage des instructions dans le cache L1.png|centre|vignette|upright=2.5|Prédécodage des instructions dans le cache L1]] Le prédécodage est particulièrement utile avec des instructions de taille variable : il permet de pré-déterminer où commencent/terminent les instructions dans une ligne de cache, indiquer leur taille, etc. Autre possibilité, le prédécodage peut indiquer s'il y a des branchements dans une ligne de cache et où ils se trouvent, ce qui est très utile pour la prédiction de branchement. Pour chaque ligne de cache, le décodage partiel fournit des informations utiles au décodeur d'instruction. Les informations pré-décodées sont soit intégrée dans la ligne de cache, soit mémorisées dans une banque séparée. En clair : une partie de la capacité totale du cache d'instruction est utilisée pour les informations de pré-décodage. Le prédécodage est donc un compromis : un cache d'instruction de plus faible capacité, mais un décodage plus simple. Le pré-décodage est surtout utile pour les instructions qui sont ré-exécutées souvent. Pour les instructions exécutées une seule fois, le gain en performance dépend de l'efficacité du préchargement et d'autres contraintes, mais ce qui est gagné lors du décodage est souvent partiellement perdu lors du prédécodage. Par contre, si une instruction est exécutée plusieurs fois, le pré-décodage est fait une seule fois, alors qu'on a un gain à chaque ré-exécution de l'instruction. ===Les sélecteurs de branchement intégrés au cache L1=== Le pré-décodage peut être utilisé afin de faciliter le travail de la prédiction de branchement. L'idée est d'incorporer une partie de la prédiction de branchement dans le cache L1 d'instruction. Une ligne de cache mémorise alors des informations de prédiction de branchement dans ses bits de contrôle. Les informations en question peuvent être des adresses de destination, ou simplement de quoi déterminer si le branchement est pris ou non. Il s'agit d'une démarche inverse à celle de la ''Fetch Target Queue'', qui découple l'unité de prédiction de branchement du cache, en insérant une mémoire FIFO entre les deux. Là, la démarche est au contraire de fusionner partiellement cache d'instruction et unité de prédiction de branchement. Les premiers processeurs AMD utilisaient cette technique, au moins dans les grandes lignes. Une ligne de cache contient potentiellement plusieurs branchements, dont la position est identifiée par le prédécodage. Pour chaque octet, la ligne de cache associe un bit de contrôle qui indique si un branchement démarre à cet octet, si c'est le premier octet d'un branchement. Le prédécodage peut identifier entre un et plusieurs branchement par ligne de cache, il y a une limite. Le prédécodage n'identifie typiquement que les 3 à 5 premiers branchements, les suivants sont ignorés, faute de place dans les bits de contrôle. Prenons par exemple une ligne de cache de 8 octets, dans laquelle on a 2 branchements de 2 octets chacun. {|class="wikitable" style="text-align:center;" |- ! colspan="16 | Ligne de cache, en octets |- | Instruction || bgcolor="#FFFF00" | Branch 1 || bgcolor="#FFFF00" | Branch 1 || Instruction || bgcolor="#FFFF00" | Branch 2 || bgcolor="#FFFF00" | Branch 2 || Instruction || Instruction |- ! colspan="16 | Bits d'identification des branchements. |- | 0 || 1 || 0 || 0 || 1 || 0 || 0 || 0 |} Il est possible d'améliorer le tout en précisant quel est le type du branchement. Par exemple, on peut distinguer les branchements inconditionnel et conditionnels, ou encore les instruction de retour de fonction. L'intérêt n'est pas évident, mais c'est lié au fait que les branchements inconditionnels sont toujours pris, et que les retour de fonction ont une adresse de destination qui est prédite par une unité de branchement séparée, le ''return adress predictor'', pas par un BTB. Deux bits suffisent pour indiquer : si c'est un branchement conditionnel, inconditionnel, un retour de fonction, ou une instruction qui n'est pas un branchement. {|class="wikitable" style="text-align:center;" |- ! colspan="16 | Ligne de cache, en octets |- | Instruction || bgcolor="#FFFF00" | Saut inconditionnel || bgcolor="#FFFF00" | Saut inconditionnel || Instruction || bgcolor="#A00000" | Branch cond || bgcolor="#A00000" | Branch cond || Instruction || bgcolor="#F0F000" | Retour de fonction |- ! colspan="16 | Bits d'identification des branchements. |- | 00 || 01 || 00 || 00 || 10 || 00 || 00 || 11 |} L'idée est alors d'ajouter, pour chaque branchement détecté, un '''sélecteur de branchement''' qui indique si le branchement est pris ou non. En clair, des informations de prédiction de branchement sont ajoutés à chaque octet de position. Intuitivement, on se dit qu'il y a seulement un bit par branchement, qui indique si le branchement est pris ou non. Les prédictions peuvent venir soit de l'unité de prédiction de branchement, soit provenir du prédécodage. Le prédécodage peut faire de la prédiction statique. Elle peut notamment détecter les branchements inconditionnels et les marquer comme pris. Elle peut aussi détecter les branchements conditionnels et le marquer comme non-pris par défaut. L'unité de prédiction de branchement met à jour les sélecteurs de branchements si besoin, pour les branchements conditionnels. ===L'incorporation du ''Branch Target Buffer'' dans le cache d'instruction=== Une première optimisation permet de se passer de ''Branch Target Buffer''. Pour rappel, celui-ci est un cache qui mémorise, pour chaque branchement, quelle est son adresse de destination. Il peut contenir d'autres informations de prédiction, mais laissons-les de côté pour le moment. L'idée est de déplacer les adresse de destination des branchements dans le cache d'instruction, dans les lignes de cache. Si une ligne de cache contient un branchement, elle mémorise l'adresse de destination de ce branchement, en plus des bits de pré-décodage. En général, les processeurs ne supportent qu'une seule adresse de destination. Si il y a plusieurs branchements dans une ligne de cache, c'est l'adresse de destination du premier branchement pris dans cette ligne de cache qui est mémorisée. Par exemple, l'AMD K5 se passe de ''Branch Target Buffer'' grâce à cela. Il faut cependant remarquer qu'à ce petit jeu, les instructions de retour de fonction sont à part. Leur adresse de destination est souvent donnée par une unité de branchement séparée, le ''return adress predictor'', séparée du ''Branch Target Buffer''. Leurs adresses de destination n'ont pas forcément besoin d'être mémorisées dans les lignes de cache. La technique décrite ici est simple à comprendre. Cependant, les processeurs AMD anciens, d'architecture K6 à K10 n'utilisaient pas cette méthode, mais une variante plus complexe, capable de prédire jusqu'à deux adresses de destination par branchement. A partir de l'architecture K6, le prédécodage déterminait la position des branchements dans les lignes de cache, dans une limite de 4 branchements par ligne de cache. Pour chaque branchement, la ligne de cache mémorisait un sélecteur de branchement, codé sur 2 bits. La valeur des bits indiquait que le branchement n'est pas pris si elle vaut 00, que c'est une instruction de retour de fonction si elle vaut 01, qu'il faut brancher à l'adresse de destination X si elle vaut 10, qu'il faut brancher à l'adresse de destination X si elle vaut 11. Les adresses de destination sont quand à elles mémorisées dans un cache séparé, appelé le ''Branch Target Cache''. Le mécanisme pour adresser ce cache à partir du cache d'instruction n'est pas très détaillé dans la documentation d'AMD. ===Les avantages et inconvénients=== L'avantage de faire ainsi est que la prédiction de branchement est plus rapide. Lire une instruction depuis le cache renvoie non seulement l'instruction lue, mais aussi des informations de prédiction de branchement. L'unité de prédiction de branchement peut alors utiliser ces informations au cycle suivant pour savoir quelle est l'instruction suivante à charger. Un défaut de cette approche est que si le branchement à prédire n'est pas dans le L1 d'instruction, aucune prédiction de branchement ne peut être faite et le préchargement ne peut pas fonctionner. C'est une limitation que n'ont pas les BTB découplées du cache L1 : elles peuvent prédire les adresses de destination et la direction d'un branchement, tant que l'entrée associée est dans le BTB. Et l'entrée peut être conservée, même si l'instruction en question a quitté le cache L1 et qu'elle est dans le L2, le L3 ou même en mémoire RAM. Les prédictions peuvent même servir à précharger les instructions utiles. Sur l'Itanium et l'AMD Opteron, une optimisation assez intéressante permet de conserver les prédictions de branchement lorsque l'un branchement est évincé du cache L1 et se retrouve dans le cache L2. En théorie, les informations de prédiction, présentes dans la ligne de cache, sont perdues lorsque le branchement est évincé. Mais ces processeurs conservent ces prédictions dans un cache séparé, appelé le '''''L2 Branch Cache'''''. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=La prédiction de branchement | prevText=La prédiction de branchement | next=Les pipelines multicycles | nextText=Les pipelines multicycles }} </noinclude> {{AutoCat}} 3m07xef7f84fu5u5yrzafwm6m6n7nie 772769 772768 2026-09-21T22:24:59Z Mewtow 31375 /* Le Branch Folding des CPU PowerPC */ 772769 wikitext text/x-wiki Les processeurs avec un pipeline sont découpés en deux sections : un ''front-end'' qui charge et décode les instructions, et un ou plusieurs ''back-end'' qui exécutent les instructions. L'exécution des micro-opérations est optimisée par des techniques que nous verrons dans la suite du cours : exécution dans le désordre, superscalarité, renommage de registre. Mais un point important est que le ''front-end'' a des optimisations dédiées. La prédiction de branchement est l'une de ces optimisation, mais elle n'est pas la seule. Ce chapitre aborde les optimisations autres que la prédiction de branchement. Nous allons voir le découplage des étages du ''front-end'', le pré-décodage et quelques améliorations liées indirectement à la prédiction de branchement. ==Le découplage du ''front-end''== Le séquenceur d'un processeur contient une unité de chargement, un décodeur et un chemin de données. Avec un pipeline, le couplage de ces structures fait que si l'une d'entre elle prend plus d'un cycle pour faire son travail, les étages précédents et/ou suivants sont stoppés. Par exemple, lors d'un défaut dans le cache d'instruction, l'ensemble stoppe. De même, si jamais le décodeur doit décoder une instruction via le micro-code, cela prend plusieurs cycles : l'unité de chargement et le cache d'instruction sont inutilisés. Même chose si jamais une instruction multicycle s’exécute dans le pipeline : cela bloque toutes les étapes précédentes. Mais il s'agit là d'un défaut inhérent aux pipelines basiques, qui relient chaque étage avec des registres. Avec un peu d'astuce, il est possible que certains étages prennent de l'avance même si l'étage suivant est bloqué. Par exemple, si le décodeur bloque le pipeline en utilisant son micro-code pendant 4-5 cycles, l'unité de chargement peut en théorie précharger à l'avance les instructions suivantes, et les mettre en attente. Ou encore, en cas de bulle de pipeline (''pipeline stall''), le décodeur peut décoder en avance des instructions et les mettre en attente tant que la bulle de pipeline est en cours. Dans les deux cas, une unité prend de l'avance et met en attente ses résultats tant que l'étage suivant est occupé. La mise en attente est réalisée en remplaçant/complémentant les registres du pipeline avec des mémoires FIFOs. Elles remplacent ou complémentant les registres de pipeline, les deux sont possibles. Si elles remplacent ces registres, si aucune mise en attente n'est requise, elles fonctionnent comme un registre de pipeline. ===La file d'instruction=== [[File:File de micro-opérations.png|vignette|upright=1|File d'instructions]] Les processeurs modernes intègrent une '''file d'instruction''', à savoir une mémoire FIFO placée entre le cache d'instruction et le décodeur d'instruction. Les instructions chargées par l'étape de chargement soient accumulées dans la file d'instructions et sont décodées quand l'unité de décodage est prête. La file d'attente permet de charger des instructions à l'avance, permettant ainsi de masquer certains accès au cache ou à la mémoire assez longs. L'idée est que les instructions s'accumulent dans la file d'instruction si le processeur exécute les instructions moins vite qu'il ne les charge. C'est généralement signe qu'il effectue une instruction multicycle et/ou qu'il effectue un accès à la mémoire. À l'inverse, la file d'attente se vide quand le processeur exécute les instructions plus vite qu'il n'en charge. C'est généralement signe qu'un défaut de cache d'instruction est en cours. La présence d'une file d'attente fait que la première situation est compensée lors de la seconde. Les temps d'attentes liées aux instructions multicycles permettent de remplir la file d'attente, qui est ensuite vidée en cas de défaut de cache. Le processeur exécute en permanence des instructions, sans interruption. Alors que sans file d'attente, les défauts de cache entraineront des temps d'attente où le processeur s’exécuterait rien. La seule limite de cette optimisation est l'influence des branchements. Lorsqu'un branchement est décodé, ce tampon d’instructions est totalement vidé de son contenu. Ce n'est ni plus ni moins ce que faisait la ''prefetch input queue'' des anciens processeurs Intel, dont nous avions parlé dans le chapitre sur l'unité de chargement et le séquenceur. ===La file de micro-opérations=== [[File:File de micro-opération.png|vignette|upright=1|File d'instruction]] L'optimisation précédente peut aussi s'appliquer entre le décodeur d'instruction et le chemin de données. Pour cela, la sortie du décodeur est reliée à une mémoire FIFO semblable à la file d'instruction. Elle mémorise les micro-opérations émises par le décodeur et les met en attente tant que le reste du pipeline n'est pas prêt. Nous l'appellerons la '''file de micro-opérations''', mais elle porte de nombreux noms et la terminologie des différents fabricants est assez confuse. Le schéma ci-contre indique que la file de micro-opérations est située en sortie de l’unité de décodage, avant l'unité d'émission et avant l'unité de renommage de registres (que nous aborderons dans quelques chapitres). La file de micro-opérations permet aux décodeurs de faire leur travail même si le reste du pipeline n'est pas prêt. Par exemple, imaginons que le processeur ne peut pas émettre de nouvelle instruction, soit car toutes les ALUs sont occupées, soit car il y a un accès mémoire qui bloque le pipeline, peu importe. Sans file de micro-opérations, tout ce qui précède l'unité d'émission devrait être totalement bloqué tant que l'instruction ne peut pas être émise. Mais avec une file de micro-opérations, le pipeline peut charger et décoder des instructions, il a juste à accumuler les instructions décodées dans la file de micro-opérations. En clair, la file de micro-opérations met en attente les instructions décodées quand des bulles de pipeline sont émises. Et à l'inverse, elle permet d'émettre des instructions quand les unités de décodage/chargement sont bloquées. Le cas classique est celui d'un défaut de cache dans le cache d'instruction. Pendant un défaut de cache, aucune instruction n'est chargée ni décodées durant quelques cycles. Sans file de micro-opérations, le processeur ne peut plus rien faire durant quelques cycles. Mais avec une file de micro-opérations, il se rattrape en émettant les instructions en attente dans la file de micro-opérations, s'il y en a. En clair, si l'unité d'émission a mis en attente des instructions, le processeur se rattrape au prochain défaut de cache d'instruction. Une autre situation où le décodeur bloque est le cas où certaines instructions mettent du temps à être décodées. C'est notamment le cas de certaines instructions complexes, dont le décodage prend facilement 2 à 3 cycles d'horloge, voire plus. Le pire est le décodage des instructions microcodées, qui peut demander plusieurs cycles. Or, le pipeline demande qu'on décode une instruction par cycle pour éviter de bloquer le pipeline. Mais ce temps de décodage peut être masqué si des micro-opérations sont en attente dans la file, elles sont exécutées pendant le décodage long. ===Le ''Loop Stream Detector''=== Les boucles sont une opportunité d'optimisation très intéressante sur les CPU avec une file de micro-opérations. L'idée est que lors d'une boucle, des instructions sont chargées, décodées et exécutées plusieurs fois de suite. Mais à chaque répétition d'une instruction, le chargement et le décodage donnent toujours le même résultat, seule l'exécution n'est pas la même (les registres renommés sont aussi différents, mais nous verrons cela dans plusieurs chapitres). L'idée est simplement de mémoriser les N dernières instructions décodées et de les ré-exécuter si besoin. Ainsi, on évite de charger/décoder une même instruction machine plusieurs fois, mais de réutiliser les micro-opérations déjà décodées. L'implémentation la plus simple utiliser la file de micro-opérations comme une sorte de pseudo-cache FIFO. La file de micro-opérations ne supprime pas les micro-opérations une fois qu'elles sont émises. Elle mémorise là où se trouve la dernière micro-opération émise, mais conserve celles qui ont déjà été émises. Un circuit annexe, appelé le '''''Loop Stream Detector''''' (LSD), détecte les boucles dans la file de micro-opérations et optimise leur exécution. Si une boucle adéquate est détectée par le ''Loop Stream Detector'', les micro-opérations de la boucle sont lues dans la file de micro-opération et sont injectées directement dans la suite du pipeline. De plus, les unités de chargement et de décodage sont désactivées pendant l’exécution de la boucle, ce qui réduit la consommation d'énergie du CPU. L'optimisation accélère les petites boucles, qui tiennent toutes entières dans la file de micro-opérations, et sous condition qu'elles s'exécutent de la même manière à chaque exécution. De telles boucles exécutent une suite de N instructions, qui reste identique à chaque itération de la boucle. Le cas le plus simple est celui d'une boucle dans laquelle il n'y a pas de branchements. Pour les boucles normales, le processeur reprend une exécution normale quand on quitte la boucle ou quand son exécution change, par exemple quand un if...else, un return ou tout autre changement de flot de contrôle a lieu. Vu que toutes ces situations impliquent un branchement qui n'a pas été pris comme avant, le processeur n'utilise plus le ''Loop Stream Detector'' en cas de mauvaise prédiction de branchement. Le LSD vise surtout à désactiver les décodeurs et l'unité de chargement lors de l'exécution d'une boucle. La désactivation peut être du ''clock gating'', voire du ''power gating'', être partielle ou totale. Dans le pire des cas, les unités de chargement peuvent continuer à charger des instructions en avance dans une file d'instruction, mais les décodeurs peuvent être désactivés. Dans le meilleur des cas, la totalité de ce qui précède la file de micro-opération est désactivé tant que la boucle s’exécute normalement. Y compris le cache de micro-opération. [[File:Loop Stream Detector.png|centre|vignette|upright=2|Loop Stream Detector]] Évidemment, la taille des boucles optimisées ainsi est limitée par la taille de la file de micro-opération, ce qui fait que l'optimisation ne fonctionne que pour des boucles de petite taille. Pour donner quelques chiffres, les processeurs ARM Cortex A15 géraient des boucles de maximum 32 micro-opérations. De plus, toute la file de micro-opération n'est pas gérée par le ''loop stream detector''. Par exemple, les processeurs avec une file de micro-opération de 64 micro-opération peuvent gérer des boucles de maximum 32 à 40 micro-opérations. Mais les contraintes principales portent sur la détection des boucles. Le ''Loop Stream Detector'' ne peut pas détecter toutes les boucles qui existent, et certaines boucles ne sont pas détectées. Par exemple, le ''Loop Stream Detector' ne peut pas détecter les boucles si un appel de fonction a lieu dans la boucle. Il y a aussi des contraintes quant au nombre de branchements à l'intérieur de la boucle et le nombre d'accès mémoire. Les CPU Intel modernes disposent d'un ''loop stream detector'', les CPU AMD en avaient sur les microarchitectures Zen 4 mais il a disparu sur la microarchitecture Zen 5. Quelques CPU ARM avaient aussi un ''loop stream detector'', notamment le Cortex A15. Il faut noter que le ''loop stream detector'' a été désactivé par des mises à jour de microcode sur quelques architectures, comme sur la microarchitecture Zen 4 d'AMD ou les CPU de microarchitecture Skylake et Kaby Lake d'Intel. Pour la microarchitecture Skylake, les raisons officielles pour cette désactivation sont un bug lié à l'interaction avec l'''hyperthreading''. Il est vraisemblable que des bugs ou des problèmes de sécurité aient amené à la désactivation sur les autres architectures. ===Le cache de micro-opérations=== Le '''cache de micro-opérations''' a le même but que le ''Loop Stream Detector'', à savoir optimiser l'exécution des boucles. La différence avec le ''Loop Stream Detector'' est qu'il y a un cache séparé de la file de micro-opérations, qui mémorise des micro-opérations décodées, dans le cas où elles soient réutilisées par la suite. La première itération d'une boucle accumule les instructions décodées dans le cache de micro-opérations, les itérations suivantes de la boucle lisent les micro-opérations adéquates dans le cache de micro-opération : on n'a pas à décoder l'instruction une nouvelle fois. Sur de nombreux processeurs, le cache de micro-opération est alimenté non pas par l'unité de décodage, mais par la file de micro-opérations. Ainsi, seules les micro-opérations émises sont copiées dans ce cache. [[File:File de micro-opérations et cache de micro-ops.png|centre|vignette|upright=2|File de micro-opérations et cache de micro-ops]] Les avantages sont les mêmes qu'avec un ''Loop Stream Detector'' : une consommation énergétique réduite, des performances légèrement améliorées. Le décodeur et l'unité de chargement sont inutiles en cas de succès dans le cache de micro-opération, ce qui fait qu'ils sont désactivés, éteints, ou du moins subissent un ''clock-gating'' temporaire. Ils ne consomment pas d'énergie, seul le cache de micro-opération utilise de l'électricité. L'avantage en termes de performance est plus faible, assez variable suivant la situation, mais aussi bien le cache de micro-opérations que le LSD ne font pas de mal. Une différence avec le cache de micro-opération est qu'une boucle doit s’exécuter à l'identique avec un ''Loop Stream Detector'', pas avec un cache de micro-opérations. Prenons l'exemple d'une boucle contenant quelques instructions suivies par un IF...ELSE. Il arrive qu'une itération de la boucle exécute le IF, alors que d'autres exécutent le ELSE. Dans ce cas, le ''Loop Stream Detector'' ne sera pas activé, car la boucle ne s’exécute pas pareil d'une itération à l'autre. Par contre, un cache de macro/micro-opération mémorisera les micro-opérations du IF et du ELSE et les fournira selon les besoins. La raison est que le cache de micro-opérations a une politique de remplacement des lignes de cache plus complexe que le FIFO, typiquement une politique LRU ou LFU approximée. Le cache de micro-opération est donc plus efficace que le ''Loop Stream Detector'', pour un cout en transistor plus élevé. Le cache de micro-opération est une voie de chargement parallèle au ''front-end'' proprement dit. L'accès au cache de micro-opération se fait lors de l'étape de chargement. Le cache de micro-opérations est adressé en envoyant le ''program counter'' sur son entrée d'adresse, en parallèle du cache d'instruction. En clair, il y a une voie qui regroupe cache d'instruction, file d'instruction et décodeur, et une seconde voie qui se résume au cache de micro-opération. Les deux voies sont accédées en parallèle. En cas de succès dans le cache de micro-opération, les micro-opérations adéquates sont lues directement depuis le cache de micro-opération. Le cache de micro-opération associe, pour chaque instruction machine, une ou plusieurs micro-opérations. Avec l'implémentation la plus simple, une ligne de cache est associée à une instruction machine. Par exemple, sur les processeurs Intel de microarchitecture Skylake, chaque ligne de cache était associée à une instruction machine et pouvait contenir de 1 à 6 micro-opérations. Une instruction devait tenir toute entière dans une ligne de cache, ce qui fait que les instructions décodées en plus de 6 micro-opérations ne pouvaient pas rentrer dans ce cache. Il existe deux méthodes différentes pour encoder les micro-opérations dans le cache de micro-opérations. La première est la plus intuitive : on mémorise les micro-opérations dans la ligne de cache, directement. Elle est utilisée sur les processeurs AMD, et sans doute sur les processeurs Intel récents. Mais les anciens processeurs Intel, comme ceux des architectures Sandy Bridge et Netburst, utilisent une autre méthode. Une ligne de cache mémorise non pas les micro-opération directement, mais un pointeur vers le ''control store'', qui indique à quelle adresse dans le micro-code se situe la micro-opération. La micro-opération est donc lue depuis le micro-code lors de son envoi au chemin de données, lors de son émission. Il faut noter que pour des raisons de performance, le cache de micro-opérations est virtuellement tagué, ce qui fait qu'il est invalidé en cas de changement de programme. Sur l'architecture Sandy Bridge, il est carrément inclus dans le cache L1, les deux sont des caches inclusifs l'un avec l'autre. Les premières implémentations étaient très limitées. Les micro-opérations devaient être séquentielles dans le code, le cache était consulté seulement après un branchement et non à chaque instruction, pour limiter la consommation d'énergie an détriment des performances. Ces limitations ne sont pas présentes sur les architectures récentes. Le cache de micro-opérations et le ''Loop Stream Detector'' font la même chose, mais certains processeurs implémentaient les deux. L'avantage est que le cache de micro-opération peut être désactivé si jamais le LSD détecte une boucle dans la file d'instruction, ce qui réduit encore plus la consommation énergétique. En pratique, l'impact sur la consommation énergétique est très difficile à mesurer, mais il rajoute de la complexité pour la conception du processeur. [[File:File de micro-opérations et cache de micro-ops - Copie.png|centre|vignette|upright=2.5|File de micro-opérations et cache de micro-ops - Copie]] ==Le préchargement d'instructions et la ''Fetch Target Queue''== Nous venons de voir qu'il est possible de découpler les étages de chargement, décodage et le chemin de données, en insérant des mémoires FIFOs dans le pipeline. Il en est de même avec l'unité de chargement elle-même. Elle est composée de deux circuits entre lesquels on peut ajouter des mémoires FIFO : une unité de calcul d'adresse et le cache d'instruction. L'unité de calcul d'adresse regroupe : l'unité de prédiction de branchement, le ''program counter'', le circuit pour incrémenter le ''program counter'', les MUX associés pour gérer les branchements. L'unité de calcul d'adresse émet les adresses des instructions à charger, qui sont consommées par le cache d'instruction. Les processeurs modernes incorporent une optimisation assez intéressante : ils découplent l'unité de calcul d'adresse de l'accès au cache d'instruction. Pour cela, ils incorporent une mémoire FIFO entre l'unité de prédiction de branchement et le cache d'instruction. Les premiers articles scientifiques, qui ont proposé cette solution, l'ont appelée la '''''Fetch Target Queue''''', abréviée FTQ. Elle accumule les adresses à lire/écrire dans le cache d'instruction, peu importe que ces adresses viennent du ''program counter'' ou de l'unité de prédiction de branchement. [[File:Fetch target queue.png|centre|vignette|upright=2.5|Fetch target queue]] Elle se remplit quand le cache d'instruction est bloqué, soit à cause d'un défaut de cache, soit à cause d'un pipeline bloqué en amont de l'unité de chargement. Par exemple, si le cache d'instruction est bloqué par un défaut de cache, l'unité de prédiction de branchement peut accumuler des prédictions à l'avance dans la FTQ, qui sont ensuite consommées par le cache d'instruction une fois qu'il est redevenu disponible. De même, si l'unité de prédiction de branchement est bloquée par un évènement quelconque, le cache d'instruction peut consommer les prédictions faites à l'avance. Une utilisation assez originale de la FTQ s'est vu sur les processeurs AMD d'architectures bulldozer. Sur cette architecture, les cœurs étaient regroupés par paquets de deux, et les deux cœurs partageaient certains circuits. Notamment, l'unité de prédiction de branchement était partagée entre les deux cœurs ! Pourtant, chaque cœur disposait de sa propre FTQ ! Un avantage de la FTQ tient dans le fait que les caches d'instructions sont pipelinés, sur le même modèle que les processeurs. On peut leur envoyer une demande de lecture/écriture par cycle, alors que chaque lecture/écriture prendra plusieurs cycles à s'effectuer. L'accès au cache d'instruction a donc une certaine latence, qui est partiellement masquée par la FTQ au point où elle ne s'exprime qu'en cas de défaut de cache assez important. Par exemple, si l'accès au cache d'instruction prend 4 cycles, une FTQ qui met en attente 4 adresses camouflera le temps d'accès au cache, tant qu'il n'y a pas de mauvaise prédiction de branchement. La FTQ est aussi très utile avec les unités de branchement modernes, qui peuvent mettre plusieurs cycles pour fournir une prédiction. Prendre de l'avance avec une FTQ amorti partiellement le temps de calcul des prédictions. : Si le cache d'instruction est multiport et accepte plusieurs accès simultanés, il peut consommer plusieurs entrées dans la FTQ à la fois. Mais l'avantage principal de la FTQ est qu'elle permet l'implémentation d'une optimisation très importante. Il y a quelques chapitres, nous avions parlé des techniques de '''préchargement d'instruction''', qui permettent de charger à l'avance des instructions dans le cache d'instruction. Nous avions volontairement laissé de côté le préchargement des instructions, pour tout un tas de raisons. Et la raison est justement que la prédiction de branchement et le préchargement des instructions sont fortement liés sur les processeurs modernes. Il est maintenant possible d'aborder le préchargement pour les instructions, d’où cette section. Notons que par préchargement des instructions, on peut parler de deux formes de préchargement, fortement différentes. La première correspond au préchargement normal, à savoir le préchargement des instructions dans le cache d'instruction L1, à partir du cache L2. Il s'agit donc d'un préchargement dans le cache d'instruction. Mais il existe aussi une autre forme de préchargement, qui consiste à précharger à l'avance des instructions dans la file d'instruction et qui a été abordée dans la section sur la ''prefetch input queue''. Les deux formes de préchargement n'ont pas lieu au même endroit dans la hiérarchie mémoire : l'une précharge du cache L2 vers le L1i, l'autre du cache L1i vers la file d'instruction (ou dans le cache de macro-opération). Mais les algorithmes utilisés pour sont sensiblement les mêmes. Aussi, nous allons les voir en même temps. Pour faire la distinction, nous parlerons de préchargement L2-L1i pour la première, de préchargement interne pour l'autre. ===Les algorithmes de préchargement d'instructions=== Les techniques basiques de préchargement consistent à charger des instructions qui suivent la dernière ligne de cache accédée. Quand on charge des instructions dans le cache d’instruction, les instructions qui suivent sont chargées automatiquement, ligne de cache par ligne de cache. il s'agit due préchargement séquentiel, la technique la plus simple de préchargement, qui profite de la localité spatiale. Elle est utilisée pour précharger des instructions du cache L2 vers le cache L1i, mais aussi pour le préchargement interne dans la file d'instructions. [[File:Branchements et préchargement séquentiel.png|centre|vignette|upright=2|Branchements et préchargement séquentiel.]] Mais un ''prefetcher'' purement séquentiel gère mal les branchements. Si un branchement est pris, les instructions de destination ne sont pas chargées, si elles ne sont pas dans la ligne de cache suivante. Pour le préchargement L2-L1i, cela ne pose pas de problèmes majeurs, au-delà de la pollution du cache L1i par des instructions inutiles. Mais pour le préchargement interne, c'est autre chose. Les instructions préchargées par erreurs doivent être supprimées pour éviter qu'elles soient décodées et exécutées, ce qui fait que la file d’instruction doit être invalidée. Il existe des techniques de préchargement plus élaborées qui marchent mieux en présence de branchements. Elles utilisent toutes une collaboration de l'unité de prédiction de branchement. Elles accèdent au ''Branch Target Buffer'', pour détecter les branchements, leur destination, etc. Le tout peut se coupler à la technique du prédécodage. Avec cette dernière, le prédécodage décode en partie les instructions lors de leur chargement dans le cache, et détecte les branchements et leur adresse de destination à ce moment-là. Ces informations sont alors mémorisées dans une table à part, ou dans le BTB. Mais la plupart des designs utilisent le BTB, par souci de simplicité. Il existe globalement deux à trois techniques principales, que nous allons voir dans ce qui suit. La première technique prédit si le branchement est pris ou non, et agit différemment si le branchement est pris ou non. Si le branchement est pris, elle précharge les instructions à partir de l'adresse de destination des branchements pris. Sinon, elle précharge les instructions suivantes avec préchargement séquentiel. Il s'agit du '''''target line prefetching''''' [[File:Target line prefetching.png|centre|vignette|upright=2|Target line prefetching.]] Une autre technique ne prédit pas les branchements et précharge à la fois les instructions suivantes avec le ''next-line prefetching'', et la ligne de cache de destination du branchement avec le ''target line prefetching''. Comme ça, peu importe que le branchement soit pris ou non, les instructions adéquates seront préchargées quand même. On appelle cette technique le '''préchargement du mauvais chemin''' (''wrong path prefetching''). [[File:Préchargement du mauvais chemin.png|centre|vignette|upright=2|Préchargement du mauvais chemin.]] Le ''target line prefetching'' est plus complexe à implémenter, car il demande de prédire les branchements. Mais elle a l'avantage de ne pas précharger inutilement deux lignes de cache par branchement, seulement une seule. Par contre, le préchargement est inutile en cas de mauvaise prédiction de branchement : non seulement on a préchargé une ligne de cache inutilement, mais en plus, la ligne de cache adéquate n'a pas été chargée. On n'a pas ce problème avec le préchargement du mauvais chemin, qui garantit que la ligne de cache adéquate est toujours préchargée. ===L'implémentation du préchargement interne, dans la file d'instruction=== Le préchargement dans la file d'instruction est généralement de type séquentiel, mais certains processeurs font autrement. Déjà, il faut remarquer que le ''target line prefetching'' correspond en réalité à la prédiction de branchement classique. L'adresse de destination est prédite, et on charge les instructions adéquates dans la file d'instruction. La prédiction de branchement, associée à une file d'instruction, est donc une forme de préchargement. Il fallait y penser. Enfin, des processeurs assez rares utilisaient le préchargement du mauvais chemin. Le préchargement du mauvais chemin demande d'utiliser deux files d'instructions séparées. L'une dans laquelle on précharge de manière séquentielle, l'autre dans laquelle on utilise la prédiction de branchement pour faire du ''target line prefetching''. Une fois que l'on sait si la prédiction de branchement était correcte, on est certain qu'une des deux files contiendra les instructions valides. Le contenu de la file adéquate est conservé, alors que l'autre est intégralement invalidée. Le choix de la bonne file se fait avec un multiplexeur. C'est approximativement la technique qui était implémentée sur le processeur de mainframe IBM 370/165, par exemple, et sur quelques modèles IBM similaires. Le problème est que cette méthode demande de charger deux instructions à chaque cycle. Cela demande donc d'utiliser un cache d'instruction multiport, avec un port par file d'instruction. Le cout en circuit d'un cache double port n'est pas négligeable. Et le gain en performance est assez faible. Le préchargement dans la file d’instruction permet d'économiser quelques cycles lors de l'accès au cache d'instruction, guère plus. Le gain est maximal lorsque les instructions préchargées ont généré un défaut de cache, qui a rapatrié les instructions adéquates pendant que le processeur exécutait les mauvaises instructions, avant que la mauvaise prédiction de branchement soit détectée. Dans ce cas, le défaut de cache a eu lieu pendant la mauvaise prédiction et sa réparation, et non après. ====La gestion des branchements successifs==== Un autre défaut de cette méthode est la présence de branchements successifs. Par exemple, si jamais on rencontre un branchement, le flux d'instructions se scinde en deux : un où le branchement est pris, un autre où il ne l'est pas. Chacun de ces flux peut lui-même contenir un branchement, et se scinder lui aussi. Et ainsi de suite. Et le processeur doit gérer cette situation en termes de préchargement. [[File:Exécution stricte 04.png|centre|vignette|upright=2|Exécution stricte]] Plusieurs solutions existent. La méthode la plus simple stoppe le chargement du flux en attendant que le premier branchement soit terminé. Cette solution est intuitive, mais est celle où on a les gains en performance les plus faibles. Elle est couramment implémentée d'une manière assez particulière, qui ne correspond pas tout à fait à un stop du chargement, mais qui utilise les lignes de cache. L'unité de préchargement est conçue pour copier des lignes de cache entières dans la file d'instruction. Le processeur (pré-)charge deux lignes de cache : celle du bon chemin, celle du mauvais chemin. Il les précharge dans deux files d'instructions, qui contiennent généralement une ligne de cache grand maximum. Le temps que l'on ait chargé les deux files d'instruction, le résultat du branchement est connu et on sait laquelle est la bonne. L'autre possibilité est d'utiliser la prédiction de branchement pour ce flux, afin de poursuivre le chargement de manière spéculative. Elle donne de bonnes performances, mais demande des unités de prédiction de branchement spéciales, dans le cas où les deux flux tombent sur un branchement en même temps. Cette technique est indirectement liée au cache de traces que nous verrons dans le chapitre sur les processeurs superscalaires. Nous n'en parlons pas ici, car ce genre de techniques est plus liée aux processeurs superscalaires qu'un processeur avec un pipeline normal. Une autre possibilité consiste à scinder ce flux en deux et charger les deux sous-flux. Cette dernière est impraticable car elle demande des caches avec un grand nombre de ports et la présence de plusieurs files d'instructions, qui sont utilisées assez rarement. [[File:Exécution stricte 01.png|centre|vignette|upright=2|Exécution stricte, seconde.]] ====Les processeurs à exécution de chemins multiples==== L'idée précédente peut en théorie être améliorée, afin de non seulement charger les instructions en provenance des deux chemins (celui du branchement pris, et celui du branchement non pris), mais aussi de les exécuter : c'est ce qu'on appelle l''''exécution stricte''' (''eager execution''). Bien sûr, on n’est pas limité à un seul branchement, mais on peut poursuivre un peu plus loin. Quelques papiers de recherche ont étudié l'idée, mais ses défauts font qu'elle n'a jamais été utilisée dans un processeur en dehors de prototypes destinés à la recherche. Le gros problème de l'exécution stricte est qu'on est limité par le nombre d'unités de calculs, de registres, etc. Autant ce serait une technique idéale sur des processeurs avec un nombre illimité de registres ou d'unités de calcul, autant ce n'est pas le cas dans le monde réel. Au bout d'un certain nombre d’embranchements, le processeur finit par ne plus pouvoir poursuivre l’exécution, par manque de ressources matérielles et doit soit stopper, soit recourir à la prédiction de branchement. Il y a le même problème avec le préchargement interne simple, quand on utilise le préchargement du mauvais chemin, comme vu juste au-dessus. ===L'implémentation matérielle du préchargement de cache L2-L1i=== Pour comprendre comment s'effectue le préchargement L2-L1i, il faut regarder comment l'unité de chargement communique avec les caches. L'unité de prédiction de branchement est généralement regroupée avec le ''program counter'' et les circuits associés (les incrémenteurs/MUX associés), pour former l'unité de chargement proprement dite. L'unité de chargement émet des adresses consommées par le cache d'instruction, qui lui-même envoie les instructions lues dans le registre d'instruction ou la file d'instructions. Le couplage de ces structures fait qu'au moindre défaut de cache d'instruction, l'ensemble stoppe. Et notamment, l'unité de prédiction de branchement stoppe en cas de défaut de cache. Même chose si jamais une instruction multicycle s’exécute dans le pipeline et bloque toutes les étapes précédentes. Les pertes de performance ne sont pas très importantes, mais elles existent. Et le préchargement se manifeste dans ces situations. Le préchargement d'instructions consiste à découpler ces structures de manière à ce qu'elles fonctionnent plus ou moins indépendamment. Le but est qu'en plus des accès normaux au cache d'instruction, l'unité de chargement envoie des informations au cache L2 ou L1i en avance, pour effectuer le préchargement. L'unité de chargement doit alors prendre de l'avance sur le cache, pour effectuer les accès au cache L2 en avance, tout en maintenant l'état normal pour effectuer les accès normaux. C'est donc plus ou moins l'unité de chargement qui s'occupe du préchargement, ou du moins les deux sont très liées. ====L'anticipation du ''program counter''==== Avec la solution la plus simple, on a une unité de chargement qui s'occupe des accès au cache d'instruction, et une unité de préchargement qui prend de l'avance sur l'unité de chargement, et communique avec le cache L2. La technique la plus basique se base sur un ''Lookahead program counter'', un second ''program counter'' qui ne fonctionne que lors d'un défaut de cache d'instruction. Il est initialisé avec le ''program counter'' lors d'un défaut de cache, puis il est incrémenté à chaque cycle et les branchements sont prédits, ce qui fait qu'il est mis à jour comme si l’exécution du programme se poursuivait, alors que le reste du processeur est mis en attente. La technique initiale utilisait ce second ''program counter'' pour accéder à une table de prédiction, qui associe à chaque valeur du ''program counter'', l'adresse des données chargées par l'instruction associée. Les adresses fournies à chaque cycle par cette table sont alors envoyées aux unités de préchargement pour qu'elles fassent leur travail. La technique permettait donc de précharger des données en cas de défaut de cache, mais pas d'instructions. Il ne s'agissait pas d'une technique de préchargement des instructions, mais de préchargement de données. La technique a ensuite été adaptée pour le chargement des instructions par Chen, Lee et Mudge. Leur idée utilisait deux unités de prédiction de branchements : une couplée à l'unité de chargement, l'autre pour le préchargement. La première utilisait le ''program counter'' normal, l'autre se déclenchait en cas de défaut de cache et utilisait un ''lookahead program counter''. Les adresses générées par le ''lookahead program counter'' étaient envoyée au cache d'instruction, sur un port de lecture séparé. La ligne de cache lue était alors prédécodée pour détecter les branchements, qui étaient prédits, et rebelote. Il est possible d'adapter la méthode pour que les adresses soient accumulées dans une mémoire FIFO, et étaient consommée par le cache d'instruction L2 pour le préchargement si la ligne de cache associée n'était pas dans le cache d’instruction. Les techniques modernes n'utilisent plus de seconde unité de prédiction de branchement, mais conservent un ''lookahead program counter''. Par contre, le BTB dispose de plusieurs ports : un pour la prédiction de branchement normale, l'autre pour le préchargement. L'unité de préchargement et l'unité de chargement accèdent toutes deux au BTB quand elles ont besoin de faire leurs prédictions, en parallèle. Typiquement, le BTB est accédé à chaque cycle pour la prédiction de branchement, à un rythme plus faible pour le préchargement. ====Le ''Fetch Directed Instruction Prefetching''==== Les processeurs modernes semblent utiliser un algorithme connu sous le nom de '''''Fetch Directed Instruction Prefetching'''''. Il utilise les adresses contenues dans la FTQ pour précharger les instructions adéquates du cache L2 vers le cache L1 d'instruction (L1i). L'unité de préchargement est placée en aval de la FTQ, elle lit son contenu, détecte quelles adresses correspondent à des lignes de cache à précharger, et envoie celles-ci au cache L2. Le préchargement du L2 vers le L1i a lieu quand le cache L2 est inutilisé, ou du moins quand il peut accepter une nouvelle lecture (dans le cas d'un cache multiport et/ou pipeliné). [[File:Fetch directed instruction prefetching.png|centre|vignette|upright=2.5|Fetch directed instruction prefetching]] On peut améliorer légèrement le design précédent sur plusieurs points. Pour éviter de polluer le cache L1 avec des lignes de caches préchargées à tort, il est possible d'ajouter un équivalent des ''stream buffer'' vus dans le chapitre sur le préchargement. Il s'agit d'une autre mémoire FIFO qui mémorise les lignes de cache préchargées. Les lignes de cache préchargées ne sont pas placées dans le cache L1i, mais dans cette file d'attente. Lors d'un accès au L1i, la file d'attente est consultée en parallèle. Si l'instruction voulue est dans la file d'attente, elle est lue depuis la file, et la ligne de cache associée est copiée dans le cache L1i. Mais c'est là une possibilité facultative. Un autre point est que l'unité de préchargement doit attendre que le cache L2 puisse accepter une nouvelle lecture pour lancer le préchargement d'une autre ligne de cache. Pour corriger cela, on ajoute une file d'attente entre le cache L2 et l'unité de préchargement, qui est évidemment une mémoire FIFO. Son utilité dépend des temps de lectures du cache L2, ainsi que de la taille de la FTQ. Elle n'est pas toujours nécessaire, certains processeurs ont un cache L2 assez lent pour qu'on ne puisse précharger qu'une seule ligne de cache avant que la FTQ soit complétement vide. Ces deux optimisations sont facultatives, mais elles étaient présentes dans l'article originel qui a proposé la technique. L'unité de préchargement doit détecter quelles sont les adresses de la FTQ qui ne sont pas déjà chargées dans le L1i. En effet, il est inutile de précharger une ligne de cache si celle-ci est déjà dans le cache L1i. L'unité de préchargement doit donc filtrer au mieux les adresses de la FTQ en deux classes : celles qui correspondent à une ligne de cache déjà dans le L1i, celles qui doivent être préchargées. Pour cela, l'unité de préchargement utilise la technique dit du '''''Cache Probe Filtering'''''. L'idée part du principe que le cache d'instruction L1 est multiport. Les ports du cache d'instruction ne sont pas toujours utilisés en même temps et il arrive qu'il y ait un port de lecture de libre. Le CPF utilise alors ce port inutilisé pour vérifier si la prochaine ligne de cache à précharger est dans le cache ou non. Si c'est le cas, on aura un succès de cache : la ligne de cache est oubliée, elle ne sera pas préchargée. Si ce n'est pas le cas on aura un défaut de cache : la ligne sera préchargée. Notez que l'on a pas besoin de lire la ligne en question, juste de vérifier les tags du cache. Dans ce cas, on peut ajouter des signaux de commande spécifiques pour le CPF, qui font une demi-lecture, qui ne vérifie que les tags, mais ne lit pas la donnée. On peut par exemple ajouter un port spécifique pour le CPF, purement en lecture et qui ne permet que de vérifier les tags. Ce port en plus a un cout en circuits plus faible qu'un port de lecture normal, mais ce n'est pas gratuit du tout. ==Le prédécodage d'instructions== La présence d'un cache d'instruction permet l'implémentation de certaines optimisations, dont la plus connue est la technique dite du '''prédécodage'''. Avec elle, lorsque les instructions sont chargées dans le cache d'instruction, elles sont partiellement décodées, grâce à un circuit séparé de l'unité de décodage d'instruction. Le décodage de l'instruction proprement dit est plus court, car une partie du travail est faite en avance, on gagne quelques cycles. [[File:Prédécodage des instructions dans le cache L1.png|centre|vignette|upright=2.5|Prédécodage des instructions dans le cache L1]] Le prédécodage est particulièrement utile avec des instructions de taille variable : il permet de pré-déterminer où commencent/terminent les instructions dans une ligne de cache, indiquer leur taille, etc. Autre possibilité, le prédécodage peut indiquer s'il y a des branchements dans une ligne de cache et où ils se trouvent, ce qui est très utile pour la prédiction de branchement. Pour chaque ligne de cache, le décodage partiel fournit des informations utiles au décodeur d'instruction. Les informations pré-décodées sont soit intégrée dans la ligne de cache, soit mémorisées dans une banque séparée. En clair : une partie de la capacité totale du cache d'instruction est utilisée pour les informations de pré-décodage. Le prédécodage est donc un compromis : un cache d'instruction de plus faible capacité, mais un décodage plus simple. Le pré-décodage est surtout utile pour les instructions qui sont ré-exécutées souvent. Pour les instructions exécutées une seule fois, le gain en performance dépend de l'efficacité du préchargement et d'autres contraintes, mais ce qui est gagné lors du décodage est souvent partiellement perdu lors du prédécodage. Par contre, si une instruction est exécutée plusieurs fois, le pré-décodage est fait une seule fois, alors qu'on a un gain à chaque ré-exécution de l'instruction. ===Les sélecteurs de branchement intégrés au cache L1=== Le pré-décodage peut être utilisé afin de faciliter le travail de la prédiction de branchement. L'idée est d'incorporer une partie de la prédiction de branchement dans le cache L1 d'instruction. Une ligne de cache mémorise alors des informations de prédiction de branchement dans ses bits de contrôle. Les informations en question peuvent être des adresses de destination, ou simplement de quoi déterminer si le branchement est pris ou non. Il s'agit d'une démarche inverse à celle de la ''Fetch Target Queue'', qui découple l'unité de prédiction de branchement du cache, en insérant une mémoire FIFO entre les deux. Là, la démarche est au contraire de fusionner partiellement cache d'instruction et unité de prédiction de branchement. Les premiers processeurs AMD utilisaient cette technique, au moins dans les grandes lignes. Une ligne de cache contient potentiellement plusieurs branchements, dont la position est identifiée par le prédécodage. Pour chaque octet, la ligne de cache associe un bit de contrôle qui indique si un branchement démarre à cet octet, si c'est le premier octet d'un branchement. Le prédécodage peut identifier entre un et plusieurs branchement par ligne de cache, il y a une limite. Le prédécodage n'identifie typiquement que les 3 à 5 premiers branchements, les suivants sont ignorés, faute de place dans les bits de contrôle. Prenons par exemple une ligne de cache de 8 octets, dans laquelle on a 2 branchements de 2 octets chacun. {|class="wikitable" style="text-align:center;" |- ! colspan="16 | Ligne de cache, en octets |- | Instruction || bgcolor="#FFFF00" | Branch 1 || bgcolor="#FFFF00" | Branch 1 || Instruction || bgcolor="#FFFF00" | Branch 2 || bgcolor="#FFFF00" | Branch 2 || Instruction || Instruction |- ! colspan="16 | Bits d'identification des branchements. |- | 0 || 1 || 0 || 0 || 1 || 0 || 0 || 0 |} Il est possible d'améliorer le tout en précisant quel est le type du branchement. Par exemple, on peut distinguer les branchements inconditionnel et conditionnels, ou encore les instruction de retour de fonction. L'intérêt n'est pas évident, mais c'est lié au fait que les branchements inconditionnels sont toujours pris, et que les retour de fonction ont une adresse de destination qui est prédite par une unité de branchement séparée, le ''return adress predictor'', pas par un BTB. Deux bits suffisent pour indiquer : si c'est un branchement conditionnel, inconditionnel, un retour de fonction, ou une instruction qui n'est pas un branchement. {|class="wikitable" style="text-align:center;" |- ! colspan="16 | Ligne de cache, en octets |- | Instruction || bgcolor="#FFFF00" | Saut inconditionnel || bgcolor="#FFFF00" | Saut inconditionnel || Instruction || bgcolor="#A00000" | Branch cond || bgcolor="#A00000" | Branch cond || Instruction || bgcolor="#F0F000" | Retour de fonction |- ! colspan="16 | Bits d'identification des branchements. |- | 00 || 01 || 00 || 00 || 10 || 00 || 00 || 11 |} L'idée est alors d'ajouter, pour chaque branchement détecté, un '''sélecteur de branchement''' qui indique si le branchement est pris ou non. En clair, des informations de prédiction de branchement sont ajoutés à chaque octet de position. Intuitivement, on se dit qu'il y a seulement un bit par branchement, qui indique si le branchement est pris ou non. Les prédictions peuvent venir soit de l'unité de prédiction de branchement, soit provenir du prédécodage. Le prédécodage peut faire de la prédiction statique. Elle peut notamment détecter les branchements inconditionnels et les marquer comme pris. Elle peut aussi détecter les branchements conditionnels et le marquer comme non-pris par défaut. L'unité de prédiction de branchement met à jour les sélecteurs de branchements si besoin, pour les branchements conditionnels. ===L'incorporation du ''Branch Target Buffer'' dans le cache d'instruction=== Une première optimisation permet de se passer de ''Branch Target Buffer''. Pour rappel, celui-ci est un cache qui mémorise, pour chaque branchement, quelle est son adresse de destination. Il peut contenir d'autres informations de prédiction, mais laissons-les de côté pour le moment. L'idée est de déplacer les adresse de destination des branchements dans le cache d'instruction, dans les lignes de cache. Si une ligne de cache contient un branchement, elle mémorise l'adresse de destination de ce branchement, en plus des bits de pré-décodage. En général, les processeurs ne supportent qu'une seule adresse de destination. Si il y a plusieurs branchements dans une ligne de cache, c'est l'adresse de destination du premier branchement pris dans cette ligne de cache qui est mémorisée. Par exemple, l'AMD K5 se passe de ''Branch Target Buffer'' grâce à cela. Il faut cependant remarquer qu'à ce petit jeu, les instructions de retour de fonction sont à part. Leur adresse de destination est souvent donnée par une unité de branchement séparée, le ''return adress predictor'', séparée du ''Branch Target Buffer''. Leurs adresses de destination n'ont pas forcément besoin d'être mémorisées dans les lignes de cache. La technique décrite ici est simple à comprendre. Cependant, les processeurs AMD anciens, d'architecture K6 à K10 n'utilisaient pas cette méthode, mais une variante plus complexe, capable de prédire jusqu'à deux adresses de destination par branchement. A partir de l'architecture K6, le prédécodage déterminait la position des branchements dans les lignes de cache, dans une limite de 4 branchements par ligne de cache. Pour chaque branchement, la ligne de cache mémorisait un sélecteur de branchement, codé sur 2 bits. La valeur des bits indiquait que le branchement n'est pas pris si elle vaut 00, que c'est une instruction de retour de fonction si elle vaut 01, qu'il faut brancher à l'adresse de destination X si elle vaut 10, qu'il faut brancher à l'adresse de destination X si elle vaut 11. Les adresses de destination sont quand à elles mémorisées dans un cache séparé, appelé le ''Branch Target Cache''. Le mécanisme pour adresser ce cache à partir du cache d'instruction n'est pas très détaillé dans la documentation d'AMD. ===Les avantages et inconvénients=== L'avantage de faire ainsi est que la prédiction de branchement est plus rapide. Lire une instruction depuis le cache renvoie non seulement l'instruction lue, mais aussi des informations de prédiction de branchement. L'unité de prédiction de branchement peut alors utiliser ces informations au cycle suivant pour savoir quelle est l'instruction suivante à charger. Un défaut de cette approche est que si le branchement à prédire n'est pas dans le L1 d'instruction, aucune prédiction de branchement ne peut être faite et le préchargement ne peut pas fonctionner. C'est une limitation que n'ont pas les BTB découplées du cache L1 : elles peuvent prédire les adresses de destination et la direction d'un branchement, tant que l'entrée associée est dans le BTB. Et l'entrée peut être conservée, même si l'instruction en question a quitté le cache L1 et qu'elle est dans le L2, le L3 ou même en mémoire RAM. Les prédictions peuvent même servir à précharger les instructions utiles. Sur l'Itanium et l'AMD Opteron, une optimisation assez intéressante permet de conserver les prédictions de branchement lorsque l'un branchement est évincé du cache L1 et se retrouve dans le cache L2. En théorie, les informations de prédiction, présentes dans la ligne de cache, sont perdues lorsque le branchement est évincé. Mais ces processeurs conservent ces prédictions dans un cache séparé, appelé le '''''L2 Branch Cache'''''. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=La prédiction de branchement | prevText=La prédiction de branchement | next=Les pipelines multicycles | nextText=Les pipelines multicycles }} </noinclude> {{AutoCat}} 961z278cml501s87tzbagyfhnd33qe5 772770 772769 2026-09-21T22:27:27Z Mewtow 31375 /* Les avantages et inconvénients */ 772770 wikitext text/x-wiki Les processeurs avec un pipeline sont découpés en deux sections : un ''front-end'' qui charge et décode les instructions, et un ou plusieurs ''back-end'' qui exécutent les instructions. L'exécution des micro-opérations est optimisée par des techniques que nous verrons dans la suite du cours : exécution dans le désordre, superscalarité, renommage de registre. Mais un point important est que le ''front-end'' a des optimisations dédiées. La prédiction de branchement est l'une de ces optimisation, mais elle n'est pas la seule. Ce chapitre aborde les optimisations autres que la prédiction de branchement. Nous allons voir le découplage des étages du ''front-end'', le pré-décodage et quelques améliorations liées indirectement à la prédiction de branchement. ==Le découplage du ''front-end''== Le séquenceur d'un processeur contient une unité de chargement, un décodeur et un chemin de données. Avec un pipeline, le couplage de ces structures fait que si l'une d'entre elle prend plus d'un cycle pour faire son travail, les étages précédents et/ou suivants sont stoppés. Par exemple, lors d'un défaut dans le cache d'instruction, l'ensemble stoppe. De même, si jamais le décodeur doit décoder une instruction via le micro-code, cela prend plusieurs cycles : l'unité de chargement et le cache d'instruction sont inutilisés. Même chose si jamais une instruction multicycle s’exécute dans le pipeline : cela bloque toutes les étapes précédentes. Mais il s'agit là d'un défaut inhérent aux pipelines basiques, qui relient chaque étage avec des registres. Avec un peu d'astuce, il est possible que certains étages prennent de l'avance même si l'étage suivant est bloqué. Par exemple, si le décodeur bloque le pipeline en utilisant son micro-code pendant 4-5 cycles, l'unité de chargement peut en théorie précharger à l'avance les instructions suivantes, et les mettre en attente. Ou encore, en cas de bulle de pipeline (''pipeline stall''), le décodeur peut décoder en avance des instructions et les mettre en attente tant que la bulle de pipeline est en cours. Dans les deux cas, une unité prend de l'avance et met en attente ses résultats tant que l'étage suivant est occupé. La mise en attente est réalisée en remplaçant/complémentant les registres du pipeline avec des mémoires FIFOs. Elles remplacent ou complémentant les registres de pipeline, les deux sont possibles. Si elles remplacent ces registres, si aucune mise en attente n'est requise, elles fonctionnent comme un registre de pipeline. ===La file d'instruction=== [[File:File de micro-opérations.png|vignette|upright=1|File d'instructions]] Les processeurs modernes intègrent une '''file d'instruction''', à savoir une mémoire FIFO placée entre le cache d'instruction et le décodeur d'instruction. Les instructions chargées par l'étape de chargement soient accumulées dans la file d'instructions et sont décodées quand l'unité de décodage est prête. La file d'attente permet de charger des instructions à l'avance, permettant ainsi de masquer certains accès au cache ou à la mémoire assez longs. L'idée est que les instructions s'accumulent dans la file d'instruction si le processeur exécute les instructions moins vite qu'il ne les charge. C'est généralement signe qu'il effectue une instruction multicycle et/ou qu'il effectue un accès à la mémoire. À l'inverse, la file d'attente se vide quand le processeur exécute les instructions plus vite qu'il n'en charge. C'est généralement signe qu'un défaut de cache d'instruction est en cours. La présence d'une file d'attente fait que la première situation est compensée lors de la seconde. Les temps d'attentes liées aux instructions multicycles permettent de remplir la file d'attente, qui est ensuite vidée en cas de défaut de cache. Le processeur exécute en permanence des instructions, sans interruption. Alors que sans file d'attente, les défauts de cache entraineront des temps d'attente où le processeur s’exécuterait rien. La seule limite de cette optimisation est l'influence des branchements. Lorsqu'un branchement est décodé, ce tampon d’instructions est totalement vidé de son contenu. Ce n'est ni plus ni moins ce que faisait la ''prefetch input queue'' des anciens processeurs Intel, dont nous avions parlé dans le chapitre sur l'unité de chargement et le séquenceur. ===La file de micro-opérations=== [[File:File de micro-opération.png|vignette|upright=1|File d'instruction]] L'optimisation précédente peut aussi s'appliquer entre le décodeur d'instruction et le chemin de données. Pour cela, la sortie du décodeur est reliée à une mémoire FIFO semblable à la file d'instruction. Elle mémorise les micro-opérations émises par le décodeur et les met en attente tant que le reste du pipeline n'est pas prêt. Nous l'appellerons la '''file de micro-opérations''', mais elle porte de nombreux noms et la terminologie des différents fabricants est assez confuse. Le schéma ci-contre indique que la file de micro-opérations est située en sortie de l’unité de décodage, avant l'unité d'émission et avant l'unité de renommage de registres (que nous aborderons dans quelques chapitres). La file de micro-opérations permet aux décodeurs de faire leur travail même si le reste du pipeline n'est pas prêt. Par exemple, imaginons que le processeur ne peut pas émettre de nouvelle instruction, soit car toutes les ALUs sont occupées, soit car il y a un accès mémoire qui bloque le pipeline, peu importe. Sans file de micro-opérations, tout ce qui précède l'unité d'émission devrait être totalement bloqué tant que l'instruction ne peut pas être émise. Mais avec une file de micro-opérations, le pipeline peut charger et décoder des instructions, il a juste à accumuler les instructions décodées dans la file de micro-opérations. En clair, la file de micro-opérations met en attente les instructions décodées quand des bulles de pipeline sont émises. Et à l'inverse, elle permet d'émettre des instructions quand les unités de décodage/chargement sont bloquées. Le cas classique est celui d'un défaut de cache dans le cache d'instruction. Pendant un défaut de cache, aucune instruction n'est chargée ni décodées durant quelques cycles. Sans file de micro-opérations, le processeur ne peut plus rien faire durant quelques cycles. Mais avec une file de micro-opérations, il se rattrape en émettant les instructions en attente dans la file de micro-opérations, s'il y en a. En clair, si l'unité d'émission a mis en attente des instructions, le processeur se rattrape au prochain défaut de cache d'instruction. Une autre situation où le décodeur bloque est le cas où certaines instructions mettent du temps à être décodées. C'est notamment le cas de certaines instructions complexes, dont le décodage prend facilement 2 à 3 cycles d'horloge, voire plus. Le pire est le décodage des instructions microcodées, qui peut demander plusieurs cycles. Or, le pipeline demande qu'on décode une instruction par cycle pour éviter de bloquer le pipeline. Mais ce temps de décodage peut être masqué si des micro-opérations sont en attente dans la file, elles sont exécutées pendant le décodage long. ===Le ''Loop Stream Detector''=== Les boucles sont une opportunité d'optimisation très intéressante sur les CPU avec une file de micro-opérations. L'idée est que lors d'une boucle, des instructions sont chargées, décodées et exécutées plusieurs fois de suite. Mais à chaque répétition d'une instruction, le chargement et le décodage donnent toujours le même résultat, seule l'exécution n'est pas la même (les registres renommés sont aussi différents, mais nous verrons cela dans plusieurs chapitres). L'idée est simplement de mémoriser les N dernières instructions décodées et de les ré-exécuter si besoin. Ainsi, on évite de charger/décoder une même instruction machine plusieurs fois, mais de réutiliser les micro-opérations déjà décodées. L'implémentation la plus simple utiliser la file de micro-opérations comme une sorte de pseudo-cache FIFO. La file de micro-opérations ne supprime pas les micro-opérations une fois qu'elles sont émises. Elle mémorise là où se trouve la dernière micro-opération émise, mais conserve celles qui ont déjà été émises. Un circuit annexe, appelé le '''''Loop Stream Detector''''' (LSD), détecte les boucles dans la file de micro-opérations et optimise leur exécution. Si une boucle adéquate est détectée par le ''Loop Stream Detector'', les micro-opérations de la boucle sont lues dans la file de micro-opération et sont injectées directement dans la suite du pipeline. De plus, les unités de chargement et de décodage sont désactivées pendant l’exécution de la boucle, ce qui réduit la consommation d'énergie du CPU. L'optimisation accélère les petites boucles, qui tiennent toutes entières dans la file de micro-opérations, et sous condition qu'elles s'exécutent de la même manière à chaque exécution. De telles boucles exécutent une suite de N instructions, qui reste identique à chaque itération de la boucle. Le cas le plus simple est celui d'une boucle dans laquelle il n'y a pas de branchements. Pour les boucles normales, le processeur reprend une exécution normale quand on quitte la boucle ou quand son exécution change, par exemple quand un if...else, un return ou tout autre changement de flot de contrôle a lieu. Vu que toutes ces situations impliquent un branchement qui n'a pas été pris comme avant, le processeur n'utilise plus le ''Loop Stream Detector'' en cas de mauvaise prédiction de branchement. Le LSD vise surtout à désactiver les décodeurs et l'unité de chargement lors de l'exécution d'une boucle. La désactivation peut être du ''clock gating'', voire du ''power gating'', être partielle ou totale. Dans le pire des cas, les unités de chargement peuvent continuer à charger des instructions en avance dans une file d'instruction, mais les décodeurs peuvent être désactivés. Dans le meilleur des cas, la totalité de ce qui précède la file de micro-opération est désactivé tant que la boucle s’exécute normalement. Y compris le cache de micro-opération. [[File:Loop Stream Detector.png|centre|vignette|upright=2|Loop Stream Detector]] Évidemment, la taille des boucles optimisées ainsi est limitée par la taille de la file de micro-opération, ce qui fait que l'optimisation ne fonctionne que pour des boucles de petite taille. Pour donner quelques chiffres, les processeurs ARM Cortex A15 géraient des boucles de maximum 32 micro-opérations. De plus, toute la file de micro-opération n'est pas gérée par le ''loop stream detector''. Par exemple, les processeurs avec une file de micro-opération de 64 micro-opération peuvent gérer des boucles de maximum 32 à 40 micro-opérations. Mais les contraintes principales portent sur la détection des boucles. Le ''Loop Stream Detector'' ne peut pas détecter toutes les boucles qui existent, et certaines boucles ne sont pas détectées. Par exemple, le ''Loop Stream Detector' ne peut pas détecter les boucles si un appel de fonction a lieu dans la boucle. Il y a aussi des contraintes quant au nombre de branchements à l'intérieur de la boucle et le nombre d'accès mémoire. Les CPU Intel modernes disposent d'un ''loop stream detector'', les CPU AMD en avaient sur les microarchitectures Zen 4 mais il a disparu sur la microarchitecture Zen 5. Quelques CPU ARM avaient aussi un ''loop stream detector'', notamment le Cortex A15. Il faut noter que le ''loop stream detector'' a été désactivé par des mises à jour de microcode sur quelques architectures, comme sur la microarchitecture Zen 4 d'AMD ou les CPU de microarchitecture Skylake et Kaby Lake d'Intel. Pour la microarchitecture Skylake, les raisons officielles pour cette désactivation sont un bug lié à l'interaction avec l'''hyperthreading''. Il est vraisemblable que des bugs ou des problèmes de sécurité aient amené à la désactivation sur les autres architectures. ===Le cache de micro-opérations=== Le '''cache de micro-opérations''' a le même but que le ''Loop Stream Detector'', à savoir optimiser l'exécution des boucles. La différence avec le ''Loop Stream Detector'' est qu'il y a un cache séparé de la file de micro-opérations, qui mémorise des micro-opérations décodées, dans le cas où elles soient réutilisées par la suite. La première itération d'une boucle accumule les instructions décodées dans le cache de micro-opérations, les itérations suivantes de la boucle lisent les micro-opérations adéquates dans le cache de micro-opération : on n'a pas à décoder l'instruction une nouvelle fois. Sur de nombreux processeurs, le cache de micro-opération est alimenté non pas par l'unité de décodage, mais par la file de micro-opérations. Ainsi, seules les micro-opérations émises sont copiées dans ce cache. [[File:File de micro-opérations et cache de micro-ops.png|centre|vignette|upright=2|File de micro-opérations et cache de micro-ops]] Les avantages sont les mêmes qu'avec un ''Loop Stream Detector'' : une consommation énergétique réduite, des performances légèrement améliorées. Le décodeur et l'unité de chargement sont inutiles en cas de succès dans le cache de micro-opération, ce qui fait qu'ils sont désactivés, éteints, ou du moins subissent un ''clock-gating'' temporaire. Ils ne consomment pas d'énergie, seul le cache de micro-opération utilise de l'électricité. L'avantage en termes de performance est plus faible, assez variable suivant la situation, mais aussi bien le cache de micro-opérations que le LSD ne font pas de mal. Une différence avec le cache de micro-opération est qu'une boucle doit s’exécuter à l'identique avec un ''Loop Stream Detector'', pas avec un cache de micro-opérations. Prenons l'exemple d'une boucle contenant quelques instructions suivies par un IF...ELSE. Il arrive qu'une itération de la boucle exécute le IF, alors que d'autres exécutent le ELSE. Dans ce cas, le ''Loop Stream Detector'' ne sera pas activé, car la boucle ne s’exécute pas pareil d'une itération à l'autre. Par contre, un cache de macro/micro-opération mémorisera les micro-opérations du IF et du ELSE et les fournira selon les besoins. La raison est que le cache de micro-opérations a une politique de remplacement des lignes de cache plus complexe que le FIFO, typiquement une politique LRU ou LFU approximée. Le cache de micro-opération est donc plus efficace que le ''Loop Stream Detector'', pour un cout en transistor plus élevé. Le cache de micro-opération est une voie de chargement parallèle au ''front-end'' proprement dit. L'accès au cache de micro-opération se fait lors de l'étape de chargement. Le cache de micro-opérations est adressé en envoyant le ''program counter'' sur son entrée d'adresse, en parallèle du cache d'instruction. En clair, il y a une voie qui regroupe cache d'instruction, file d'instruction et décodeur, et une seconde voie qui se résume au cache de micro-opération. Les deux voies sont accédées en parallèle. En cas de succès dans le cache de micro-opération, les micro-opérations adéquates sont lues directement depuis le cache de micro-opération. Le cache de micro-opération associe, pour chaque instruction machine, une ou plusieurs micro-opérations. Avec l'implémentation la plus simple, une ligne de cache est associée à une instruction machine. Par exemple, sur les processeurs Intel de microarchitecture Skylake, chaque ligne de cache était associée à une instruction machine et pouvait contenir de 1 à 6 micro-opérations. Une instruction devait tenir toute entière dans une ligne de cache, ce qui fait que les instructions décodées en plus de 6 micro-opérations ne pouvaient pas rentrer dans ce cache. Il existe deux méthodes différentes pour encoder les micro-opérations dans le cache de micro-opérations. La première est la plus intuitive : on mémorise les micro-opérations dans la ligne de cache, directement. Elle est utilisée sur les processeurs AMD, et sans doute sur les processeurs Intel récents. Mais les anciens processeurs Intel, comme ceux des architectures Sandy Bridge et Netburst, utilisent une autre méthode. Une ligne de cache mémorise non pas les micro-opération directement, mais un pointeur vers le ''control store'', qui indique à quelle adresse dans le micro-code se situe la micro-opération. La micro-opération est donc lue depuis le micro-code lors de son envoi au chemin de données, lors de son émission. Il faut noter que pour des raisons de performance, le cache de micro-opérations est virtuellement tagué, ce qui fait qu'il est invalidé en cas de changement de programme. Sur l'architecture Sandy Bridge, il est carrément inclus dans le cache L1, les deux sont des caches inclusifs l'un avec l'autre. Les premières implémentations étaient très limitées. Les micro-opérations devaient être séquentielles dans le code, le cache était consulté seulement après un branchement et non à chaque instruction, pour limiter la consommation d'énergie an détriment des performances. Ces limitations ne sont pas présentes sur les architectures récentes. Le cache de micro-opérations et le ''Loop Stream Detector'' font la même chose, mais certains processeurs implémentaient les deux. L'avantage est que le cache de micro-opération peut être désactivé si jamais le LSD détecte une boucle dans la file d'instruction, ce qui réduit encore plus la consommation énergétique. En pratique, l'impact sur la consommation énergétique est très difficile à mesurer, mais il rajoute de la complexité pour la conception du processeur. [[File:File de micro-opérations et cache de micro-ops - Copie.png|centre|vignette|upright=2.5|File de micro-opérations et cache de micro-ops - Copie]] ==Le préchargement d'instructions et la ''Fetch Target Queue''== Nous venons de voir qu'il est possible de découpler les étages de chargement, décodage et le chemin de données, en insérant des mémoires FIFOs dans le pipeline. Il en est de même avec l'unité de chargement elle-même. Elle est composée de deux circuits entre lesquels on peut ajouter des mémoires FIFO : une unité de calcul d'adresse et le cache d'instruction. L'unité de calcul d'adresse regroupe : l'unité de prédiction de branchement, le ''program counter'', le circuit pour incrémenter le ''program counter'', les MUX associés pour gérer les branchements. L'unité de calcul d'adresse émet les adresses des instructions à charger, qui sont consommées par le cache d'instruction. Les processeurs modernes incorporent une optimisation assez intéressante : ils découplent l'unité de calcul d'adresse de l'accès au cache d'instruction. Pour cela, ils incorporent une mémoire FIFO entre l'unité de prédiction de branchement et le cache d'instruction. Les premiers articles scientifiques, qui ont proposé cette solution, l'ont appelée la '''''Fetch Target Queue''''', abréviée FTQ. Elle accumule les adresses à lire/écrire dans le cache d'instruction, peu importe que ces adresses viennent du ''program counter'' ou de l'unité de prédiction de branchement. [[File:Fetch target queue.png|centre|vignette|upright=2.5|Fetch target queue]] Elle se remplit quand le cache d'instruction est bloqué, soit à cause d'un défaut de cache, soit à cause d'un pipeline bloqué en amont de l'unité de chargement. Par exemple, si le cache d'instruction est bloqué par un défaut de cache, l'unité de prédiction de branchement peut accumuler des prédictions à l'avance dans la FTQ, qui sont ensuite consommées par le cache d'instruction une fois qu'il est redevenu disponible. De même, si l'unité de prédiction de branchement est bloquée par un évènement quelconque, le cache d'instruction peut consommer les prédictions faites à l'avance. Une utilisation assez originale de la FTQ s'est vu sur les processeurs AMD d'architectures bulldozer. Sur cette architecture, les cœurs étaient regroupés par paquets de deux, et les deux cœurs partageaient certains circuits. Notamment, l'unité de prédiction de branchement était partagée entre les deux cœurs ! Pourtant, chaque cœur disposait de sa propre FTQ ! Un avantage de la FTQ tient dans le fait que les caches d'instructions sont pipelinés, sur le même modèle que les processeurs. On peut leur envoyer une demande de lecture/écriture par cycle, alors que chaque lecture/écriture prendra plusieurs cycles à s'effectuer. L'accès au cache d'instruction a donc une certaine latence, qui est partiellement masquée par la FTQ au point où elle ne s'exprime qu'en cas de défaut de cache assez important. Par exemple, si l'accès au cache d'instruction prend 4 cycles, une FTQ qui met en attente 4 adresses camouflera le temps d'accès au cache, tant qu'il n'y a pas de mauvaise prédiction de branchement. La FTQ est aussi très utile avec les unités de branchement modernes, qui peuvent mettre plusieurs cycles pour fournir une prédiction. Prendre de l'avance avec une FTQ amorti partiellement le temps de calcul des prédictions. : Si le cache d'instruction est multiport et accepte plusieurs accès simultanés, il peut consommer plusieurs entrées dans la FTQ à la fois. Mais l'avantage principal de la FTQ est qu'elle permet l'implémentation d'une optimisation très importante. Il y a quelques chapitres, nous avions parlé des techniques de '''préchargement d'instruction''', qui permettent de charger à l'avance des instructions dans le cache d'instruction. Nous avions volontairement laissé de côté le préchargement des instructions, pour tout un tas de raisons. Et la raison est justement que la prédiction de branchement et le préchargement des instructions sont fortement liés sur les processeurs modernes. Il est maintenant possible d'aborder le préchargement pour les instructions, d’où cette section. Notons que par préchargement des instructions, on peut parler de deux formes de préchargement, fortement différentes. La première correspond au préchargement normal, à savoir le préchargement des instructions dans le cache d'instruction L1, à partir du cache L2. Il s'agit donc d'un préchargement dans le cache d'instruction. Mais il existe aussi une autre forme de préchargement, qui consiste à précharger à l'avance des instructions dans la file d'instruction et qui a été abordée dans la section sur la ''prefetch input queue''. Les deux formes de préchargement n'ont pas lieu au même endroit dans la hiérarchie mémoire : l'une précharge du cache L2 vers le L1i, l'autre du cache L1i vers la file d'instruction (ou dans le cache de macro-opération). Mais les algorithmes utilisés pour sont sensiblement les mêmes. Aussi, nous allons les voir en même temps. Pour faire la distinction, nous parlerons de préchargement L2-L1i pour la première, de préchargement interne pour l'autre. ===Les algorithmes de préchargement d'instructions=== Les techniques basiques de préchargement consistent à charger des instructions qui suivent la dernière ligne de cache accédée. Quand on charge des instructions dans le cache d’instruction, les instructions qui suivent sont chargées automatiquement, ligne de cache par ligne de cache. il s'agit due préchargement séquentiel, la technique la plus simple de préchargement, qui profite de la localité spatiale. Elle est utilisée pour précharger des instructions du cache L2 vers le cache L1i, mais aussi pour le préchargement interne dans la file d'instructions. [[File:Branchements et préchargement séquentiel.png|centre|vignette|upright=2|Branchements et préchargement séquentiel.]] Mais un ''prefetcher'' purement séquentiel gère mal les branchements. Si un branchement est pris, les instructions de destination ne sont pas chargées, si elles ne sont pas dans la ligne de cache suivante. Pour le préchargement L2-L1i, cela ne pose pas de problèmes majeurs, au-delà de la pollution du cache L1i par des instructions inutiles. Mais pour le préchargement interne, c'est autre chose. Les instructions préchargées par erreurs doivent être supprimées pour éviter qu'elles soient décodées et exécutées, ce qui fait que la file d’instruction doit être invalidée. Il existe des techniques de préchargement plus élaborées qui marchent mieux en présence de branchements. Elles utilisent toutes une collaboration de l'unité de prédiction de branchement. Elles accèdent au ''Branch Target Buffer'', pour détecter les branchements, leur destination, etc. Le tout peut se coupler à la technique du prédécodage. Avec cette dernière, le prédécodage décode en partie les instructions lors de leur chargement dans le cache, et détecte les branchements et leur adresse de destination à ce moment-là. Ces informations sont alors mémorisées dans une table à part, ou dans le BTB. Mais la plupart des designs utilisent le BTB, par souci de simplicité. Il existe globalement deux à trois techniques principales, que nous allons voir dans ce qui suit. La première technique prédit si le branchement est pris ou non, et agit différemment si le branchement est pris ou non. Si le branchement est pris, elle précharge les instructions à partir de l'adresse de destination des branchements pris. Sinon, elle précharge les instructions suivantes avec préchargement séquentiel. Il s'agit du '''''target line prefetching''''' [[File:Target line prefetching.png|centre|vignette|upright=2|Target line prefetching.]] Une autre technique ne prédit pas les branchements et précharge à la fois les instructions suivantes avec le ''next-line prefetching'', et la ligne de cache de destination du branchement avec le ''target line prefetching''. Comme ça, peu importe que le branchement soit pris ou non, les instructions adéquates seront préchargées quand même. On appelle cette technique le '''préchargement du mauvais chemin''' (''wrong path prefetching''). [[File:Préchargement du mauvais chemin.png|centre|vignette|upright=2|Préchargement du mauvais chemin.]] Le ''target line prefetching'' est plus complexe à implémenter, car il demande de prédire les branchements. Mais elle a l'avantage de ne pas précharger inutilement deux lignes de cache par branchement, seulement une seule. Par contre, le préchargement est inutile en cas de mauvaise prédiction de branchement : non seulement on a préchargé une ligne de cache inutilement, mais en plus, la ligne de cache adéquate n'a pas été chargée. On n'a pas ce problème avec le préchargement du mauvais chemin, qui garantit que la ligne de cache adéquate est toujours préchargée. ===L'implémentation du préchargement interne, dans la file d'instruction=== Le préchargement dans la file d'instruction est généralement de type séquentiel, mais certains processeurs font autrement. Déjà, il faut remarquer que le ''target line prefetching'' correspond en réalité à la prédiction de branchement classique. L'adresse de destination est prédite, et on charge les instructions adéquates dans la file d'instruction. La prédiction de branchement, associée à une file d'instruction, est donc une forme de préchargement. Il fallait y penser. Enfin, des processeurs assez rares utilisaient le préchargement du mauvais chemin. Le préchargement du mauvais chemin demande d'utiliser deux files d'instructions séparées. L'une dans laquelle on précharge de manière séquentielle, l'autre dans laquelle on utilise la prédiction de branchement pour faire du ''target line prefetching''. Une fois que l'on sait si la prédiction de branchement était correcte, on est certain qu'une des deux files contiendra les instructions valides. Le contenu de la file adéquate est conservé, alors que l'autre est intégralement invalidée. Le choix de la bonne file se fait avec un multiplexeur. C'est approximativement la technique qui était implémentée sur le processeur de mainframe IBM 370/165, par exemple, et sur quelques modèles IBM similaires. Le problème est que cette méthode demande de charger deux instructions à chaque cycle. Cela demande donc d'utiliser un cache d'instruction multiport, avec un port par file d'instruction. Le cout en circuit d'un cache double port n'est pas négligeable. Et le gain en performance est assez faible. Le préchargement dans la file d’instruction permet d'économiser quelques cycles lors de l'accès au cache d'instruction, guère plus. Le gain est maximal lorsque les instructions préchargées ont généré un défaut de cache, qui a rapatrié les instructions adéquates pendant que le processeur exécutait les mauvaises instructions, avant que la mauvaise prédiction de branchement soit détectée. Dans ce cas, le défaut de cache a eu lieu pendant la mauvaise prédiction et sa réparation, et non après. ====La gestion des branchements successifs==== Un autre défaut de cette méthode est la présence de branchements successifs. Par exemple, si jamais on rencontre un branchement, le flux d'instructions se scinde en deux : un où le branchement est pris, un autre où il ne l'est pas. Chacun de ces flux peut lui-même contenir un branchement, et se scinder lui aussi. Et ainsi de suite. Et le processeur doit gérer cette situation en termes de préchargement. [[File:Exécution stricte 04.png|centre|vignette|upright=2|Exécution stricte]] Plusieurs solutions existent. La méthode la plus simple stoppe le chargement du flux en attendant que le premier branchement soit terminé. Cette solution est intuitive, mais est celle où on a les gains en performance les plus faibles. Elle est couramment implémentée d'une manière assez particulière, qui ne correspond pas tout à fait à un stop du chargement, mais qui utilise les lignes de cache. L'unité de préchargement est conçue pour copier des lignes de cache entières dans la file d'instruction. Le processeur (pré-)charge deux lignes de cache : celle du bon chemin, celle du mauvais chemin. Il les précharge dans deux files d'instructions, qui contiennent généralement une ligne de cache grand maximum. Le temps que l'on ait chargé les deux files d'instruction, le résultat du branchement est connu et on sait laquelle est la bonne. L'autre possibilité est d'utiliser la prédiction de branchement pour ce flux, afin de poursuivre le chargement de manière spéculative. Elle donne de bonnes performances, mais demande des unités de prédiction de branchement spéciales, dans le cas où les deux flux tombent sur un branchement en même temps. Cette technique est indirectement liée au cache de traces que nous verrons dans le chapitre sur les processeurs superscalaires. Nous n'en parlons pas ici, car ce genre de techniques est plus liée aux processeurs superscalaires qu'un processeur avec un pipeline normal. Une autre possibilité consiste à scinder ce flux en deux et charger les deux sous-flux. Cette dernière est impraticable car elle demande des caches avec un grand nombre de ports et la présence de plusieurs files d'instructions, qui sont utilisées assez rarement. [[File:Exécution stricte 01.png|centre|vignette|upright=2|Exécution stricte, seconde.]] ====Les processeurs à exécution de chemins multiples==== L'idée précédente peut en théorie être améliorée, afin de non seulement charger les instructions en provenance des deux chemins (celui du branchement pris, et celui du branchement non pris), mais aussi de les exécuter : c'est ce qu'on appelle l''''exécution stricte''' (''eager execution''). Bien sûr, on n’est pas limité à un seul branchement, mais on peut poursuivre un peu plus loin. Quelques papiers de recherche ont étudié l'idée, mais ses défauts font qu'elle n'a jamais été utilisée dans un processeur en dehors de prototypes destinés à la recherche. Le gros problème de l'exécution stricte est qu'on est limité par le nombre d'unités de calculs, de registres, etc. Autant ce serait une technique idéale sur des processeurs avec un nombre illimité de registres ou d'unités de calcul, autant ce n'est pas le cas dans le monde réel. Au bout d'un certain nombre d’embranchements, le processeur finit par ne plus pouvoir poursuivre l’exécution, par manque de ressources matérielles et doit soit stopper, soit recourir à la prédiction de branchement. Il y a le même problème avec le préchargement interne simple, quand on utilise le préchargement du mauvais chemin, comme vu juste au-dessus. ===L'implémentation matérielle du préchargement de cache L2-L1i=== Pour comprendre comment s'effectue le préchargement L2-L1i, il faut regarder comment l'unité de chargement communique avec les caches. L'unité de prédiction de branchement est généralement regroupée avec le ''program counter'' et les circuits associés (les incrémenteurs/MUX associés), pour former l'unité de chargement proprement dite. L'unité de chargement émet des adresses consommées par le cache d'instruction, qui lui-même envoie les instructions lues dans le registre d'instruction ou la file d'instructions. Le couplage de ces structures fait qu'au moindre défaut de cache d'instruction, l'ensemble stoppe. Et notamment, l'unité de prédiction de branchement stoppe en cas de défaut de cache. Même chose si jamais une instruction multicycle s’exécute dans le pipeline et bloque toutes les étapes précédentes. Les pertes de performance ne sont pas très importantes, mais elles existent. Et le préchargement se manifeste dans ces situations. Le préchargement d'instructions consiste à découpler ces structures de manière à ce qu'elles fonctionnent plus ou moins indépendamment. Le but est qu'en plus des accès normaux au cache d'instruction, l'unité de chargement envoie des informations au cache L2 ou L1i en avance, pour effectuer le préchargement. L'unité de chargement doit alors prendre de l'avance sur le cache, pour effectuer les accès au cache L2 en avance, tout en maintenant l'état normal pour effectuer les accès normaux. C'est donc plus ou moins l'unité de chargement qui s'occupe du préchargement, ou du moins les deux sont très liées. ====L'anticipation du ''program counter''==== Avec la solution la plus simple, on a une unité de chargement qui s'occupe des accès au cache d'instruction, et une unité de préchargement qui prend de l'avance sur l'unité de chargement, et communique avec le cache L2. La technique la plus basique se base sur un ''Lookahead program counter'', un second ''program counter'' qui ne fonctionne que lors d'un défaut de cache d'instruction. Il est initialisé avec le ''program counter'' lors d'un défaut de cache, puis il est incrémenté à chaque cycle et les branchements sont prédits, ce qui fait qu'il est mis à jour comme si l’exécution du programme se poursuivait, alors que le reste du processeur est mis en attente. La technique initiale utilisait ce second ''program counter'' pour accéder à une table de prédiction, qui associe à chaque valeur du ''program counter'', l'adresse des données chargées par l'instruction associée. Les adresses fournies à chaque cycle par cette table sont alors envoyées aux unités de préchargement pour qu'elles fassent leur travail. La technique permettait donc de précharger des données en cas de défaut de cache, mais pas d'instructions. Il ne s'agissait pas d'une technique de préchargement des instructions, mais de préchargement de données. La technique a ensuite été adaptée pour le chargement des instructions par Chen, Lee et Mudge. Leur idée utilisait deux unités de prédiction de branchements : une couplée à l'unité de chargement, l'autre pour le préchargement. La première utilisait le ''program counter'' normal, l'autre se déclenchait en cas de défaut de cache et utilisait un ''lookahead program counter''. Les adresses générées par le ''lookahead program counter'' étaient envoyée au cache d'instruction, sur un port de lecture séparé. La ligne de cache lue était alors prédécodée pour détecter les branchements, qui étaient prédits, et rebelote. Il est possible d'adapter la méthode pour que les adresses soient accumulées dans une mémoire FIFO, et étaient consommée par le cache d'instruction L2 pour le préchargement si la ligne de cache associée n'était pas dans le cache d’instruction. Les techniques modernes n'utilisent plus de seconde unité de prédiction de branchement, mais conservent un ''lookahead program counter''. Par contre, le BTB dispose de plusieurs ports : un pour la prédiction de branchement normale, l'autre pour le préchargement. L'unité de préchargement et l'unité de chargement accèdent toutes deux au BTB quand elles ont besoin de faire leurs prédictions, en parallèle. Typiquement, le BTB est accédé à chaque cycle pour la prédiction de branchement, à un rythme plus faible pour le préchargement. ====Le ''Fetch Directed Instruction Prefetching''==== Les processeurs modernes semblent utiliser un algorithme connu sous le nom de '''''Fetch Directed Instruction Prefetching'''''. Il utilise les adresses contenues dans la FTQ pour précharger les instructions adéquates du cache L2 vers le cache L1 d'instruction (L1i). L'unité de préchargement est placée en aval de la FTQ, elle lit son contenu, détecte quelles adresses correspondent à des lignes de cache à précharger, et envoie celles-ci au cache L2. Le préchargement du L2 vers le L1i a lieu quand le cache L2 est inutilisé, ou du moins quand il peut accepter une nouvelle lecture (dans le cas d'un cache multiport et/ou pipeliné). [[File:Fetch directed instruction prefetching.png|centre|vignette|upright=2.5|Fetch directed instruction prefetching]] On peut améliorer légèrement le design précédent sur plusieurs points. Pour éviter de polluer le cache L1 avec des lignes de caches préchargées à tort, il est possible d'ajouter un équivalent des ''stream buffer'' vus dans le chapitre sur le préchargement. Il s'agit d'une autre mémoire FIFO qui mémorise les lignes de cache préchargées. Les lignes de cache préchargées ne sont pas placées dans le cache L1i, mais dans cette file d'attente. Lors d'un accès au L1i, la file d'attente est consultée en parallèle. Si l'instruction voulue est dans la file d'attente, elle est lue depuis la file, et la ligne de cache associée est copiée dans le cache L1i. Mais c'est là une possibilité facultative. Un autre point est que l'unité de préchargement doit attendre que le cache L2 puisse accepter une nouvelle lecture pour lancer le préchargement d'une autre ligne de cache. Pour corriger cela, on ajoute une file d'attente entre le cache L2 et l'unité de préchargement, qui est évidemment une mémoire FIFO. Son utilité dépend des temps de lectures du cache L2, ainsi que de la taille de la FTQ. Elle n'est pas toujours nécessaire, certains processeurs ont un cache L2 assez lent pour qu'on ne puisse précharger qu'une seule ligne de cache avant que la FTQ soit complétement vide. Ces deux optimisations sont facultatives, mais elles étaient présentes dans l'article originel qui a proposé la technique. L'unité de préchargement doit détecter quelles sont les adresses de la FTQ qui ne sont pas déjà chargées dans le L1i. En effet, il est inutile de précharger une ligne de cache si celle-ci est déjà dans le cache L1i. L'unité de préchargement doit donc filtrer au mieux les adresses de la FTQ en deux classes : celles qui correspondent à une ligne de cache déjà dans le L1i, celles qui doivent être préchargées. Pour cela, l'unité de préchargement utilise la technique dit du '''''Cache Probe Filtering'''''. L'idée part du principe que le cache d'instruction L1 est multiport. Les ports du cache d'instruction ne sont pas toujours utilisés en même temps et il arrive qu'il y ait un port de lecture de libre. Le CPF utilise alors ce port inutilisé pour vérifier si la prochaine ligne de cache à précharger est dans le cache ou non. Si c'est le cas, on aura un succès de cache : la ligne de cache est oubliée, elle ne sera pas préchargée. Si ce n'est pas le cas on aura un défaut de cache : la ligne sera préchargée. Notez que l'on a pas besoin de lire la ligne en question, juste de vérifier les tags du cache. Dans ce cas, on peut ajouter des signaux de commande spécifiques pour le CPF, qui font une demi-lecture, qui ne vérifie que les tags, mais ne lit pas la donnée. On peut par exemple ajouter un port spécifique pour le CPF, purement en lecture et qui ne permet que de vérifier les tags. Ce port en plus a un cout en circuits plus faible qu'un port de lecture normal, mais ce n'est pas gratuit du tout. ==Le prédécodage d'instructions== La présence d'un cache d'instruction permet l'implémentation de certaines optimisations, dont la plus connue est la technique dite du '''prédécodage'''. Avec elle, lorsque les instructions sont chargées dans le cache d'instruction, elles sont partiellement décodées, grâce à un circuit séparé de l'unité de décodage d'instruction. Le décodage de l'instruction proprement dit est plus court, car une partie du travail est faite en avance, on gagne quelques cycles. [[File:Prédécodage des instructions dans le cache L1.png|centre|vignette|upright=2.5|Prédécodage des instructions dans le cache L1]] Le prédécodage est particulièrement utile avec des instructions de taille variable : il permet de pré-déterminer où commencent/terminent les instructions dans une ligne de cache, indiquer leur taille, etc. Autre possibilité, le prédécodage peut indiquer s'il y a des branchements dans une ligne de cache et où ils se trouvent, ce qui est très utile pour la prédiction de branchement. Pour chaque ligne de cache, le décodage partiel fournit des informations utiles au décodeur d'instruction. Les informations pré-décodées sont soit intégrée dans la ligne de cache, soit mémorisées dans une banque séparée. En clair : une partie de la capacité totale du cache d'instruction est utilisée pour les informations de pré-décodage. Le prédécodage est donc un compromis : un cache d'instruction de plus faible capacité, mais un décodage plus simple. Le pré-décodage est surtout utile pour les instructions qui sont ré-exécutées souvent. Pour les instructions exécutées une seule fois, le gain en performance dépend de l'efficacité du préchargement et d'autres contraintes, mais ce qui est gagné lors du décodage est souvent partiellement perdu lors du prédécodage. Par contre, si une instruction est exécutée plusieurs fois, le pré-décodage est fait une seule fois, alors qu'on a un gain à chaque ré-exécution de l'instruction. ===Les sélecteurs de branchement intégrés au cache L1=== Le pré-décodage peut être utilisé afin de faciliter le travail de la prédiction de branchement. L'idée est d'incorporer une partie de la prédiction de branchement dans le cache L1 d'instruction. Une ligne de cache mémorise alors des informations de prédiction de branchement dans ses bits de contrôle. Les informations en question peuvent être des adresses de destination, ou simplement de quoi déterminer si le branchement est pris ou non. Il s'agit d'une démarche inverse à celle de la ''Fetch Target Queue'', qui découple l'unité de prédiction de branchement du cache, en insérant une mémoire FIFO entre les deux. Là, la démarche est au contraire de fusionner partiellement cache d'instruction et unité de prédiction de branchement. Les premiers processeurs AMD utilisaient cette technique, au moins dans les grandes lignes. Une ligne de cache contient potentiellement plusieurs branchements, dont la position est identifiée par le prédécodage. Pour chaque octet, la ligne de cache associe un bit de contrôle qui indique si un branchement démarre à cet octet, si c'est le premier octet d'un branchement. Le prédécodage peut identifier entre un et plusieurs branchement par ligne de cache, il y a une limite. Le prédécodage n'identifie typiquement que les 3 à 5 premiers branchements, les suivants sont ignorés, faute de place dans les bits de contrôle. Prenons par exemple une ligne de cache de 8 octets, dans laquelle on a 2 branchements de 2 octets chacun. {|class="wikitable" style="text-align:center;" |- ! colspan="16 | Ligne de cache, en octets |- | Instruction || bgcolor="#FFFF00" | Branch 1 || bgcolor="#FFFF00" | Branch 1 || Instruction || bgcolor="#FFFF00" | Branch 2 || bgcolor="#FFFF00" | Branch 2 || Instruction || Instruction |- ! colspan="16 | Bits d'identification des branchements. |- | 0 || 1 || 0 || 0 || 1 || 0 || 0 || 0 |} Il est possible d'améliorer le tout en précisant quel est le type du branchement. Par exemple, on peut distinguer les branchements inconditionnel et conditionnels, ou encore les instruction de retour de fonction. L'intérêt n'est pas évident, mais c'est lié au fait que les branchements inconditionnels sont toujours pris, et que les retour de fonction ont une adresse de destination qui est prédite par une unité de branchement séparée, le ''return adress predictor'', pas par un BTB. Deux bits suffisent pour indiquer : si c'est un branchement conditionnel, inconditionnel, un retour de fonction, ou une instruction qui n'est pas un branchement. {|class="wikitable" style="text-align:center;" |- ! colspan="16 | Ligne de cache, en octets |- | Instruction || bgcolor="#FFFF00" | Saut inconditionnel || bgcolor="#FFFF00" | Saut inconditionnel || Instruction || bgcolor="#A00000" | Branch cond || bgcolor="#A00000" | Branch cond || Instruction || bgcolor="#F0F000" | Retour de fonction |- ! colspan="16 | Bits d'identification des branchements. |- | 00 || 01 || 00 || 00 || 10 || 00 || 00 || 11 |} L'idée est alors d'ajouter, pour chaque branchement détecté, un '''sélecteur de branchement''' qui indique si le branchement est pris ou non. En clair, des informations de prédiction de branchement sont ajoutés à chaque octet de position. Intuitivement, on se dit qu'il y a seulement un bit par branchement, qui indique si le branchement est pris ou non. Les prédictions peuvent venir soit de l'unité de prédiction de branchement, soit provenir du prédécodage. Le prédécodage peut faire de la prédiction statique. Elle peut notamment détecter les branchements inconditionnels et les marquer comme pris. Elle peut aussi détecter les branchements conditionnels et le marquer comme non-pris par défaut. L'unité de prédiction de branchement met à jour les sélecteurs de branchements si besoin, pour les branchements conditionnels. ===L'incorporation du ''Branch Target Buffer'' dans le cache d'instruction=== Une première optimisation permet de se passer de ''Branch Target Buffer''. Pour rappel, celui-ci est un cache qui mémorise, pour chaque branchement, quelle est son adresse de destination. Il peut contenir d'autres informations de prédiction, mais laissons-les de côté pour le moment. L'idée est de déplacer les adresse de destination des branchements dans le cache d'instruction, dans les lignes de cache. Si une ligne de cache contient un branchement, elle mémorise l'adresse de destination de ce branchement, en plus des bits de pré-décodage. En général, les processeurs ne supportent qu'une seule adresse de destination. Si il y a plusieurs branchements dans une ligne de cache, c'est l'adresse de destination du premier branchement pris dans cette ligne de cache qui est mémorisée. Par exemple, l'AMD K5 se passe de ''Branch Target Buffer'' grâce à cela. Il faut cependant remarquer qu'à ce petit jeu, les instructions de retour de fonction sont à part. Leur adresse de destination est souvent donnée par une unité de branchement séparée, le ''return adress predictor'', séparée du ''Branch Target Buffer''. Leurs adresses de destination n'ont pas forcément besoin d'être mémorisées dans les lignes de cache. La technique décrite ici est simple à comprendre. Cependant, les processeurs AMD anciens, d'architecture K6 à K10 n'utilisaient pas cette méthode, mais une variante plus complexe, capable de prédire jusqu'à deux adresses de destination par branchement. A partir de l'architecture K6, le prédécodage déterminait la position des branchements dans les lignes de cache, dans une limite de 4 branchements par ligne de cache. Pour chaque branchement, la ligne de cache mémorisait un sélecteur de branchement, codé sur 2 bits. La valeur des bits indiquait que le branchement n'est pas pris si elle vaut 00, que c'est une instruction de retour de fonction si elle vaut 01, qu'il faut brancher à l'adresse de destination X si elle vaut 10, qu'il faut brancher à l'adresse de destination X si elle vaut 11. Les adresses de destination sont quand à elles mémorisées dans un cache séparé, appelé le ''Branch Target Cache''. Le mécanisme pour adresser ce cache à partir du cache d'instruction n'est pas très détaillé dans la documentation d'AMD. ===Les avantages et inconvénients=== L'avantage de faire ainsi est que la prédiction de branchement est plus rapide. Lire une instruction depuis le cache renvoie non seulement l'instruction lue, mais aussi des informations de prédiction de branchement. L'unité de prédiction de branchement peut alors utiliser ces informations au cycle suivant pour savoir quelle est l'instruction suivante à charger. Un défaut de cette approche est que si le branchement à prédire n'est pas dans le L1 d'instruction, aucune prédiction de branchement ne peut être faite et le préchargement ne peut pas fonctionner. C'est une limitation que n'ont pas les BTB découplées du cache L1 : elles peuvent prédire les adresses de destination et la direction d'un branchement, tant que l'entrée associée est dans le BTB. Et l'entrée peut être conservée, même si l'instruction en question a quitté le cache L1 et qu'elle est dans le L2, le L3 ou même en mémoire RAM. Les prédictions peuvent même servir à précharger les instructions utiles. Sur l'Itanium et l'AMD Opteron, une optimisation assez intéressante permet de conserver les prédictions de branchement lorsque l'un branchement est évincé du cache L1 et se retrouve dans le cache L2. En théorie, les informations de prédiction, présentes dans la ligne de cache, sont perdues lorsque le branchement est évincé. Mais ces processeurs conservent ces prédictions dans un cache séparé, appelé le '''''L2 Branch Cache'''''. ==Le ''Branch Folding'' des CPU PowerPC== Le '''''Branch Folding''''' est une optimisation qui permet d'exécuter les branchements en avance, pendant qu'ils sont encore dans la file d'instruction. Pour cela, une unité spécialisée scanne la file d'instruction pour y trouver les branchements. Quand elle tombe sur un branchement, elle l'exécute en avance et altère le contenu de la file d'instruction pour remplacer les instructions chargées à tord. L'unité en question s'appelle l''''unité de branchements anticipés'''. Pour les branchements inconditionnels, elle peut les "exécuter directement". Elle retire le branchement et le remplace par l'instruction de destination du branchement. Le ''program counter'' est aussi altéré en avance, ce qui fait que les instructions adéquates sont ensuite chargées dans la file d'instruction. Pour les branchements conditionnels, elle se base sur les résultats de la prédiction de branchement pour savoir s'ils sont pris ou non. Si le branchement est considéré comme pris, il est traité comme un branchement inconditionnel : il est remplacé par l'instruction de destination et le ''program counter'' est altéré. SI le branchement est non-pris, il suffit de ne rien faire. Les premiers processeurs PowerPC utilisaient cette optimisation. Ils avaient une file d'instruction de 8 instructions, et l'unité de branchement scannait les 4 premières instructions. L'unité de branchements anticipés contenait les registres nécessaires pour gérer les branchements. Elle contenait précissément le ''link register'' pour des appels de procédures, le ''Count Target Register'' et le ''Count Register'' utilisés pour les boucles. Il contient aussi un additionneur pour calculer les adresses des branchements relatifs. L'unité de branchements anticipés doit identifier les branchements, ce qui demande de décoder partiellement les instructions en attente. Une solution pratique est d'utiliser la technique du pré-décodage d'instruction, de la section précédente. Le pré-décodage est parfait pour identifier les branchements dans un bloc d'instruction. Les informations prédécodées peuvent alors être utilisées pour remplir la file d'instruction. Chaque instruction dans la file d'instruction est associée à un bit qui indique si c'est un branchement ou non. L'unité de branchement anticipé peut alors détecter les branchements avec un simple encodeur à priorité. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=La prédiction de branchement | prevText=La prédiction de branchement | next=Les pipelines multicycles | nextText=Les pipelines multicycles }} </noinclude> {{AutoCat}} afe0dyv0oj0gznsfxouw0cvroscj45e 772774 772770 2026-09-21T22:42:27Z Mewtow 31375 /* Le Branch Folding des CPU PowerPC */ 772774 wikitext text/x-wiki Les processeurs avec un pipeline sont découpés en deux sections : un ''front-end'' qui charge et décode les instructions, et un ou plusieurs ''back-end'' qui exécutent les instructions. L'exécution des micro-opérations est optimisée par des techniques que nous verrons dans la suite du cours : exécution dans le désordre, superscalarité, renommage de registre. Mais un point important est que le ''front-end'' a des optimisations dédiées. La prédiction de branchement est l'une de ces optimisation, mais elle n'est pas la seule. Ce chapitre aborde les optimisations autres que la prédiction de branchement. Nous allons voir le découplage des étages du ''front-end'', le pré-décodage et quelques améliorations liées indirectement à la prédiction de branchement. ==Le découplage du ''front-end''== Le séquenceur d'un processeur contient une unité de chargement, un décodeur et un chemin de données. Avec un pipeline, le couplage de ces structures fait que si l'une d'entre elle prend plus d'un cycle pour faire son travail, les étages précédents et/ou suivants sont stoppés. Par exemple, lors d'un défaut dans le cache d'instruction, l'ensemble stoppe. De même, si jamais le décodeur doit décoder une instruction via le micro-code, cela prend plusieurs cycles : l'unité de chargement et le cache d'instruction sont inutilisés. Même chose si jamais une instruction multicycle s’exécute dans le pipeline : cela bloque toutes les étapes précédentes. Mais il s'agit là d'un défaut inhérent aux pipelines basiques, qui relient chaque étage avec des registres. Avec un peu d'astuce, il est possible que certains étages prennent de l'avance même si l'étage suivant est bloqué. Par exemple, si le décodeur bloque le pipeline en utilisant son micro-code pendant 4-5 cycles, l'unité de chargement peut en théorie précharger à l'avance les instructions suivantes, et les mettre en attente. Ou encore, en cas de bulle de pipeline (''pipeline stall''), le décodeur peut décoder en avance des instructions et les mettre en attente tant que la bulle de pipeline est en cours. Dans les deux cas, une unité prend de l'avance et met en attente ses résultats tant que l'étage suivant est occupé. La mise en attente est réalisée en remplaçant/complémentant les registres du pipeline avec des mémoires FIFOs. Elles remplacent ou complémentant les registres de pipeline, les deux sont possibles. Si elles remplacent ces registres, si aucune mise en attente n'est requise, elles fonctionnent comme un registre de pipeline. ===La file d'instruction=== [[File:File de micro-opérations.png|vignette|upright=1|File d'instructions]] Les processeurs modernes intègrent une '''file d'instruction''', à savoir une mémoire FIFO placée entre le cache d'instruction et le décodeur d'instruction. Les instructions chargées par l'étape de chargement soient accumulées dans la file d'instructions et sont décodées quand l'unité de décodage est prête. La file d'attente permet de charger des instructions à l'avance, permettant ainsi de masquer certains accès au cache ou à la mémoire assez longs. L'idée est que les instructions s'accumulent dans la file d'instruction si le processeur exécute les instructions moins vite qu'il ne les charge. C'est généralement signe qu'il effectue une instruction multicycle et/ou qu'il effectue un accès à la mémoire. À l'inverse, la file d'attente se vide quand le processeur exécute les instructions plus vite qu'il n'en charge. C'est généralement signe qu'un défaut de cache d'instruction est en cours. La présence d'une file d'attente fait que la première situation est compensée lors de la seconde. Les temps d'attentes liées aux instructions multicycles permettent de remplir la file d'attente, qui est ensuite vidée en cas de défaut de cache. Le processeur exécute en permanence des instructions, sans interruption. Alors que sans file d'attente, les défauts de cache entraineront des temps d'attente où le processeur s’exécuterait rien. La seule limite de cette optimisation est l'influence des branchements. Lorsqu'un branchement est décodé, ce tampon d’instructions est totalement vidé de son contenu. Ce n'est ni plus ni moins ce que faisait la ''prefetch input queue'' des anciens processeurs Intel, dont nous avions parlé dans le chapitre sur l'unité de chargement et le séquenceur. ===La file de micro-opérations=== [[File:File de micro-opération.png|vignette|upright=1|File d'instruction]] L'optimisation précédente peut aussi s'appliquer entre le décodeur d'instruction et le chemin de données. Pour cela, la sortie du décodeur est reliée à une mémoire FIFO semblable à la file d'instruction. Elle mémorise les micro-opérations émises par le décodeur et les met en attente tant que le reste du pipeline n'est pas prêt. Nous l'appellerons la '''file de micro-opérations''', mais elle porte de nombreux noms et la terminologie des différents fabricants est assez confuse. Le schéma ci-contre indique que la file de micro-opérations est située en sortie de l’unité de décodage, avant l'unité d'émission et avant l'unité de renommage de registres (que nous aborderons dans quelques chapitres). La file de micro-opérations permet aux décodeurs de faire leur travail même si le reste du pipeline n'est pas prêt. Par exemple, imaginons que le processeur ne peut pas émettre de nouvelle instruction, soit car toutes les ALUs sont occupées, soit car il y a un accès mémoire qui bloque le pipeline, peu importe. Sans file de micro-opérations, tout ce qui précède l'unité d'émission devrait être totalement bloqué tant que l'instruction ne peut pas être émise. Mais avec une file de micro-opérations, le pipeline peut charger et décoder des instructions, il a juste à accumuler les instructions décodées dans la file de micro-opérations. En clair, la file de micro-opérations met en attente les instructions décodées quand des bulles de pipeline sont émises. Et à l'inverse, elle permet d'émettre des instructions quand les unités de décodage/chargement sont bloquées. Le cas classique est celui d'un défaut de cache dans le cache d'instruction. Pendant un défaut de cache, aucune instruction n'est chargée ni décodées durant quelques cycles. Sans file de micro-opérations, le processeur ne peut plus rien faire durant quelques cycles. Mais avec une file de micro-opérations, il se rattrape en émettant les instructions en attente dans la file de micro-opérations, s'il y en a. En clair, si l'unité d'émission a mis en attente des instructions, le processeur se rattrape au prochain défaut de cache d'instruction. Une autre situation où le décodeur bloque est le cas où certaines instructions mettent du temps à être décodées. C'est notamment le cas de certaines instructions complexes, dont le décodage prend facilement 2 à 3 cycles d'horloge, voire plus. Le pire est le décodage des instructions microcodées, qui peut demander plusieurs cycles. Or, le pipeline demande qu'on décode une instruction par cycle pour éviter de bloquer le pipeline. Mais ce temps de décodage peut être masqué si des micro-opérations sont en attente dans la file, elles sont exécutées pendant le décodage long. ===Le ''Loop Stream Detector''=== Les boucles sont une opportunité d'optimisation très intéressante sur les CPU avec une file de micro-opérations. L'idée est que lors d'une boucle, des instructions sont chargées, décodées et exécutées plusieurs fois de suite. Mais à chaque répétition d'une instruction, le chargement et le décodage donnent toujours le même résultat, seule l'exécution n'est pas la même (les registres renommés sont aussi différents, mais nous verrons cela dans plusieurs chapitres). L'idée est simplement de mémoriser les N dernières instructions décodées et de les ré-exécuter si besoin. Ainsi, on évite de charger/décoder une même instruction machine plusieurs fois, mais de réutiliser les micro-opérations déjà décodées. L'implémentation la plus simple utiliser la file de micro-opérations comme une sorte de pseudo-cache FIFO. La file de micro-opérations ne supprime pas les micro-opérations une fois qu'elles sont émises. Elle mémorise là où se trouve la dernière micro-opération émise, mais conserve celles qui ont déjà été émises. Un circuit annexe, appelé le '''''Loop Stream Detector''''' (LSD), détecte les boucles dans la file de micro-opérations et optimise leur exécution. Si une boucle adéquate est détectée par le ''Loop Stream Detector'', les micro-opérations de la boucle sont lues dans la file de micro-opération et sont injectées directement dans la suite du pipeline. De plus, les unités de chargement et de décodage sont désactivées pendant l’exécution de la boucle, ce qui réduit la consommation d'énergie du CPU. L'optimisation accélère les petites boucles, qui tiennent toutes entières dans la file de micro-opérations, et sous condition qu'elles s'exécutent de la même manière à chaque exécution. De telles boucles exécutent une suite de N instructions, qui reste identique à chaque itération de la boucle. Le cas le plus simple est celui d'une boucle dans laquelle il n'y a pas de branchements. Pour les boucles normales, le processeur reprend une exécution normale quand on quitte la boucle ou quand son exécution change, par exemple quand un if...else, un return ou tout autre changement de flot de contrôle a lieu. Vu que toutes ces situations impliquent un branchement qui n'a pas été pris comme avant, le processeur n'utilise plus le ''Loop Stream Detector'' en cas de mauvaise prédiction de branchement. Le LSD vise surtout à désactiver les décodeurs et l'unité de chargement lors de l'exécution d'une boucle. La désactivation peut être du ''clock gating'', voire du ''power gating'', être partielle ou totale. Dans le pire des cas, les unités de chargement peuvent continuer à charger des instructions en avance dans une file d'instruction, mais les décodeurs peuvent être désactivés. Dans le meilleur des cas, la totalité de ce qui précède la file de micro-opération est désactivé tant que la boucle s’exécute normalement. Y compris le cache de micro-opération. [[File:Loop Stream Detector.png|centre|vignette|upright=2|Loop Stream Detector]] Évidemment, la taille des boucles optimisées ainsi est limitée par la taille de la file de micro-opération, ce qui fait que l'optimisation ne fonctionne que pour des boucles de petite taille. Pour donner quelques chiffres, les processeurs ARM Cortex A15 géraient des boucles de maximum 32 micro-opérations. De plus, toute la file de micro-opération n'est pas gérée par le ''loop stream detector''. Par exemple, les processeurs avec une file de micro-opération de 64 micro-opération peuvent gérer des boucles de maximum 32 à 40 micro-opérations. Mais les contraintes principales portent sur la détection des boucles. Le ''Loop Stream Detector'' ne peut pas détecter toutes les boucles qui existent, et certaines boucles ne sont pas détectées. Par exemple, le ''Loop Stream Detector' ne peut pas détecter les boucles si un appel de fonction a lieu dans la boucle. Il y a aussi des contraintes quant au nombre de branchements à l'intérieur de la boucle et le nombre d'accès mémoire. Les CPU Intel modernes disposent d'un ''loop stream detector'', les CPU AMD en avaient sur les microarchitectures Zen 4 mais il a disparu sur la microarchitecture Zen 5. Quelques CPU ARM avaient aussi un ''loop stream detector'', notamment le Cortex A15. Il faut noter que le ''loop stream detector'' a été désactivé par des mises à jour de microcode sur quelques architectures, comme sur la microarchitecture Zen 4 d'AMD ou les CPU de microarchitecture Skylake et Kaby Lake d'Intel. Pour la microarchitecture Skylake, les raisons officielles pour cette désactivation sont un bug lié à l'interaction avec l'''hyperthreading''. Il est vraisemblable que des bugs ou des problèmes de sécurité aient amené à la désactivation sur les autres architectures. ===Le cache de micro-opérations=== Le '''cache de micro-opérations''' a le même but que le ''Loop Stream Detector'', à savoir optimiser l'exécution des boucles. La différence avec le ''Loop Stream Detector'' est qu'il y a un cache séparé de la file de micro-opérations, qui mémorise des micro-opérations décodées, dans le cas où elles soient réutilisées par la suite. La première itération d'une boucle accumule les instructions décodées dans le cache de micro-opérations, les itérations suivantes de la boucle lisent les micro-opérations adéquates dans le cache de micro-opération : on n'a pas à décoder l'instruction une nouvelle fois. Sur de nombreux processeurs, le cache de micro-opération est alimenté non pas par l'unité de décodage, mais par la file de micro-opérations. Ainsi, seules les micro-opérations émises sont copiées dans ce cache. [[File:File de micro-opérations et cache de micro-ops.png|centre|vignette|upright=2|File de micro-opérations et cache de micro-ops]] Les avantages sont les mêmes qu'avec un ''Loop Stream Detector'' : une consommation énergétique réduite, des performances légèrement améliorées. Le décodeur et l'unité de chargement sont inutiles en cas de succès dans le cache de micro-opération, ce qui fait qu'ils sont désactivés, éteints, ou du moins subissent un ''clock-gating'' temporaire. Ils ne consomment pas d'énergie, seul le cache de micro-opération utilise de l'électricité. L'avantage en termes de performance est plus faible, assez variable suivant la situation, mais aussi bien le cache de micro-opérations que le LSD ne font pas de mal. Une différence avec le cache de micro-opération est qu'une boucle doit s’exécuter à l'identique avec un ''Loop Stream Detector'', pas avec un cache de micro-opérations. Prenons l'exemple d'une boucle contenant quelques instructions suivies par un IF...ELSE. Il arrive qu'une itération de la boucle exécute le IF, alors que d'autres exécutent le ELSE. Dans ce cas, le ''Loop Stream Detector'' ne sera pas activé, car la boucle ne s’exécute pas pareil d'une itération à l'autre. Par contre, un cache de macro/micro-opération mémorisera les micro-opérations du IF et du ELSE et les fournira selon les besoins. La raison est que le cache de micro-opérations a une politique de remplacement des lignes de cache plus complexe que le FIFO, typiquement une politique LRU ou LFU approximée. Le cache de micro-opération est donc plus efficace que le ''Loop Stream Detector'', pour un cout en transistor plus élevé. Le cache de micro-opération est une voie de chargement parallèle au ''front-end'' proprement dit. L'accès au cache de micro-opération se fait lors de l'étape de chargement. Le cache de micro-opérations est adressé en envoyant le ''program counter'' sur son entrée d'adresse, en parallèle du cache d'instruction. En clair, il y a une voie qui regroupe cache d'instruction, file d'instruction et décodeur, et une seconde voie qui se résume au cache de micro-opération. Les deux voies sont accédées en parallèle. En cas de succès dans le cache de micro-opération, les micro-opérations adéquates sont lues directement depuis le cache de micro-opération. Le cache de micro-opération associe, pour chaque instruction machine, une ou plusieurs micro-opérations. Avec l'implémentation la plus simple, une ligne de cache est associée à une instruction machine. Par exemple, sur les processeurs Intel de microarchitecture Skylake, chaque ligne de cache était associée à une instruction machine et pouvait contenir de 1 à 6 micro-opérations. Une instruction devait tenir toute entière dans une ligne de cache, ce qui fait que les instructions décodées en plus de 6 micro-opérations ne pouvaient pas rentrer dans ce cache. Il existe deux méthodes différentes pour encoder les micro-opérations dans le cache de micro-opérations. La première est la plus intuitive : on mémorise les micro-opérations dans la ligne de cache, directement. Elle est utilisée sur les processeurs AMD, et sans doute sur les processeurs Intel récents. Mais les anciens processeurs Intel, comme ceux des architectures Sandy Bridge et Netburst, utilisent une autre méthode. Une ligne de cache mémorise non pas les micro-opération directement, mais un pointeur vers le ''control store'', qui indique à quelle adresse dans le micro-code se situe la micro-opération. La micro-opération est donc lue depuis le micro-code lors de son envoi au chemin de données, lors de son émission. Il faut noter que pour des raisons de performance, le cache de micro-opérations est virtuellement tagué, ce qui fait qu'il est invalidé en cas de changement de programme. Sur l'architecture Sandy Bridge, il est carrément inclus dans le cache L1, les deux sont des caches inclusifs l'un avec l'autre. Les premières implémentations étaient très limitées. Les micro-opérations devaient être séquentielles dans le code, le cache était consulté seulement après un branchement et non à chaque instruction, pour limiter la consommation d'énergie an détriment des performances. Ces limitations ne sont pas présentes sur les architectures récentes. Le cache de micro-opérations et le ''Loop Stream Detector'' font la même chose, mais certains processeurs implémentaient les deux. L'avantage est que le cache de micro-opération peut être désactivé si jamais le LSD détecte une boucle dans la file d'instruction, ce qui réduit encore plus la consommation énergétique. En pratique, l'impact sur la consommation énergétique est très difficile à mesurer, mais il rajoute de la complexité pour la conception du processeur. [[File:File de micro-opérations et cache de micro-ops - Copie.png|centre|vignette|upright=2.5|File de micro-opérations et cache de micro-ops - Copie]] ==Le préchargement d'instructions et la ''Fetch Target Queue''== Nous venons de voir qu'il est possible de découpler les étages de chargement, décodage et le chemin de données, en insérant des mémoires FIFOs dans le pipeline. Il en est de même avec l'unité de chargement elle-même. Elle est composée de deux circuits entre lesquels on peut ajouter des mémoires FIFO : une unité de calcul d'adresse et le cache d'instruction. L'unité de calcul d'adresse regroupe : l'unité de prédiction de branchement, le ''program counter'', le circuit pour incrémenter le ''program counter'', les MUX associés pour gérer les branchements. L'unité de calcul d'adresse émet les adresses des instructions à charger, qui sont consommées par le cache d'instruction. Les processeurs modernes incorporent une optimisation assez intéressante : ils découplent l'unité de calcul d'adresse de l'accès au cache d'instruction. Pour cela, ils incorporent une mémoire FIFO entre l'unité de prédiction de branchement et le cache d'instruction. Les premiers articles scientifiques, qui ont proposé cette solution, l'ont appelée la '''''Fetch Target Queue''''', abréviée FTQ. Elle accumule les adresses à lire/écrire dans le cache d'instruction, peu importe que ces adresses viennent du ''program counter'' ou de l'unité de prédiction de branchement. [[File:Fetch target queue.png|centre|vignette|upright=2.5|Fetch target queue]] Elle se remplit quand le cache d'instruction est bloqué, soit à cause d'un défaut de cache, soit à cause d'un pipeline bloqué en amont de l'unité de chargement. Par exemple, si le cache d'instruction est bloqué par un défaut de cache, l'unité de prédiction de branchement peut accumuler des prédictions à l'avance dans la FTQ, qui sont ensuite consommées par le cache d'instruction une fois qu'il est redevenu disponible. De même, si l'unité de prédiction de branchement est bloquée par un évènement quelconque, le cache d'instruction peut consommer les prédictions faites à l'avance. Une utilisation assez originale de la FTQ s'est vu sur les processeurs AMD d'architectures bulldozer. Sur cette architecture, les cœurs étaient regroupés par paquets de deux, et les deux cœurs partageaient certains circuits. Notamment, l'unité de prédiction de branchement était partagée entre les deux cœurs ! Pourtant, chaque cœur disposait de sa propre FTQ ! Un avantage de la FTQ tient dans le fait que les caches d'instructions sont pipelinés, sur le même modèle que les processeurs. On peut leur envoyer une demande de lecture/écriture par cycle, alors que chaque lecture/écriture prendra plusieurs cycles à s'effectuer. L'accès au cache d'instruction a donc une certaine latence, qui est partiellement masquée par la FTQ au point où elle ne s'exprime qu'en cas de défaut de cache assez important. Par exemple, si l'accès au cache d'instruction prend 4 cycles, une FTQ qui met en attente 4 adresses camouflera le temps d'accès au cache, tant qu'il n'y a pas de mauvaise prédiction de branchement. La FTQ est aussi très utile avec les unités de branchement modernes, qui peuvent mettre plusieurs cycles pour fournir une prédiction. Prendre de l'avance avec une FTQ amorti partiellement le temps de calcul des prédictions. : Si le cache d'instruction est multiport et accepte plusieurs accès simultanés, il peut consommer plusieurs entrées dans la FTQ à la fois. Mais l'avantage principal de la FTQ est qu'elle permet l'implémentation d'une optimisation très importante. Il y a quelques chapitres, nous avions parlé des techniques de '''préchargement d'instruction''', qui permettent de charger à l'avance des instructions dans le cache d'instruction. Nous avions volontairement laissé de côté le préchargement des instructions, pour tout un tas de raisons. Et la raison est justement que la prédiction de branchement et le préchargement des instructions sont fortement liés sur les processeurs modernes. Il est maintenant possible d'aborder le préchargement pour les instructions, d’où cette section. Notons que par préchargement des instructions, on peut parler de deux formes de préchargement, fortement différentes. La première correspond au préchargement normal, à savoir le préchargement des instructions dans le cache d'instruction L1, à partir du cache L2. Il s'agit donc d'un préchargement dans le cache d'instruction. Mais il existe aussi une autre forme de préchargement, qui consiste à précharger à l'avance des instructions dans la file d'instruction et qui a été abordée dans la section sur la ''prefetch input queue''. Les deux formes de préchargement n'ont pas lieu au même endroit dans la hiérarchie mémoire : l'une précharge du cache L2 vers le L1i, l'autre du cache L1i vers la file d'instruction (ou dans le cache de macro-opération). Mais les algorithmes utilisés pour sont sensiblement les mêmes. Aussi, nous allons les voir en même temps. Pour faire la distinction, nous parlerons de préchargement L2-L1i pour la première, de préchargement interne pour l'autre. ===Les algorithmes de préchargement d'instructions=== Les techniques basiques de préchargement consistent à charger des instructions qui suivent la dernière ligne de cache accédée. Quand on charge des instructions dans le cache d’instruction, les instructions qui suivent sont chargées automatiquement, ligne de cache par ligne de cache. il s'agit due préchargement séquentiel, la technique la plus simple de préchargement, qui profite de la localité spatiale. Elle est utilisée pour précharger des instructions du cache L2 vers le cache L1i, mais aussi pour le préchargement interne dans la file d'instructions. [[File:Branchements et préchargement séquentiel.png|centre|vignette|upright=2|Branchements et préchargement séquentiel.]] Mais un ''prefetcher'' purement séquentiel gère mal les branchements. Si un branchement est pris, les instructions de destination ne sont pas chargées, si elles ne sont pas dans la ligne de cache suivante. Pour le préchargement L2-L1i, cela ne pose pas de problèmes majeurs, au-delà de la pollution du cache L1i par des instructions inutiles. Mais pour le préchargement interne, c'est autre chose. Les instructions préchargées par erreurs doivent être supprimées pour éviter qu'elles soient décodées et exécutées, ce qui fait que la file d’instruction doit être invalidée. Il existe des techniques de préchargement plus élaborées qui marchent mieux en présence de branchements. Elles utilisent toutes une collaboration de l'unité de prédiction de branchement. Elles accèdent au ''Branch Target Buffer'', pour détecter les branchements, leur destination, etc. Le tout peut se coupler à la technique du prédécodage. Avec cette dernière, le prédécodage décode en partie les instructions lors de leur chargement dans le cache, et détecte les branchements et leur adresse de destination à ce moment-là. Ces informations sont alors mémorisées dans une table à part, ou dans le BTB. Mais la plupart des designs utilisent le BTB, par souci de simplicité. Il existe globalement deux à trois techniques principales, que nous allons voir dans ce qui suit. La première technique prédit si le branchement est pris ou non, et agit différemment si le branchement est pris ou non. Si le branchement est pris, elle précharge les instructions à partir de l'adresse de destination des branchements pris. Sinon, elle précharge les instructions suivantes avec préchargement séquentiel. Il s'agit du '''''target line prefetching''''' [[File:Target line prefetching.png|centre|vignette|upright=2|Target line prefetching.]] Une autre technique ne prédit pas les branchements et précharge à la fois les instructions suivantes avec le ''next-line prefetching'', et la ligne de cache de destination du branchement avec le ''target line prefetching''. Comme ça, peu importe que le branchement soit pris ou non, les instructions adéquates seront préchargées quand même. On appelle cette technique le '''préchargement du mauvais chemin''' (''wrong path prefetching''). [[File:Préchargement du mauvais chemin.png|centre|vignette|upright=2|Préchargement du mauvais chemin.]] Le ''target line prefetching'' est plus complexe à implémenter, car il demande de prédire les branchements. Mais elle a l'avantage de ne pas précharger inutilement deux lignes de cache par branchement, seulement une seule. Par contre, le préchargement est inutile en cas de mauvaise prédiction de branchement : non seulement on a préchargé une ligne de cache inutilement, mais en plus, la ligne de cache adéquate n'a pas été chargée. On n'a pas ce problème avec le préchargement du mauvais chemin, qui garantit que la ligne de cache adéquate est toujours préchargée. ===L'implémentation du préchargement interne, dans la file d'instruction=== Le préchargement dans la file d'instruction est généralement de type séquentiel, mais certains processeurs font autrement. Déjà, il faut remarquer que le ''target line prefetching'' correspond en réalité à la prédiction de branchement classique. L'adresse de destination est prédite, et on charge les instructions adéquates dans la file d'instruction. La prédiction de branchement, associée à une file d'instruction, est donc une forme de préchargement. Il fallait y penser. Enfin, des processeurs assez rares utilisaient le préchargement du mauvais chemin. Le préchargement du mauvais chemin demande d'utiliser deux files d'instructions séparées. L'une dans laquelle on précharge de manière séquentielle, l'autre dans laquelle on utilise la prédiction de branchement pour faire du ''target line prefetching''. Une fois que l'on sait si la prédiction de branchement était correcte, on est certain qu'une des deux files contiendra les instructions valides. Le contenu de la file adéquate est conservé, alors que l'autre est intégralement invalidée. Le choix de la bonne file se fait avec un multiplexeur. C'est approximativement la technique qui était implémentée sur le processeur de mainframe IBM 370/165, par exemple, et sur quelques modèles IBM similaires. Le problème est que cette méthode demande de charger deux instructions à chaque cycle. Cela demande donc d'utiliser un cache d'instruction multiport, avec un port par file d'instruction. Le cout en circuit d'un cache double port n'est pas négligeable. Et le gain en performance est assez faible. Le préchargement dans la file d’instruction permet d'économiser quelques cycles lors de l'accès au cache d'instruction, guère plus. Le gain est maximal lorsque les instructions préchargées ont généré un défaut de cache, qui a rapatrié les instructions adéquates pendant que le processeur exécutait les mauvaises instructions, avant que la mauvaise prédiction de branchement soit détectée. Dans ce cas, le défaut de cache a eu lieu pendant la mauvaise prédiction et sa réparation, et non après. ====La gestion des branchements successifs==== Un autre défaut de cette méthode est la présence de branchements successifs. Par exemple, si jamais on rencontre un branchement, le flux d'instructions se scinde en deux : un où le branchement est pris, un autre où il ne l'est pas. Chacun de ces flux peut lui-même contenir un branchement, et se scinder lui aussi. Et ainsi de suite. Et le processeur doit gérer cette situation en termes de préchargement. [[File:Exécution stricte 04.png|centre|vignette|upright=2|Exécution stricte]] Plusieurs solutions existent. La méthode la plus simple stoppe le chargement du flux en attendant que le premier branchement soit terminé. Cette solution est intuitive, mais est celle où on a les gains en performance les plus faibles. Elle est couramment implémentée d'une manière assez particulière, qui ne correspond pas tout à fait à un stop du chargement, mais qui utilise les lignes de cache. L'unité de préchargement est conçue pour copier des lignes de cache entières dans la file d'instruction. Le processeur (pré-)charge deux lignes de cache : celle du bon chemin, celle du mauvais chemin. Il les précharge dans deux files d'instructions, qui contiennent généralement une ligne de cache grand maximum. Le temps que l'on ait chargé les deux files d'instruction, le résultat du branchement est connu et on sait laquelle est la bonne. L'autre possibilité est d'utiliser la prédiction de branchement pour ce flux, afin de poursuivre le chargement de manière spéculative. Elle donne de bonnes performances, mais demande des unités de prédiction de branchement spéciales, dans le cas où les deux flux tombent sur un branchement en même temps. Cette technique est indirectement liée au cache de traces que nous verrons dans le chapitre sur les processeurs superscalaires. Nous n'en parlons pas ici, car ce genre de techniques est plus liée aux processeurs superscalaires qu'un processeur avec un pipeline normal. Une autre possibilité consiste à scinder ce flux en deux et charger les deux sous-flux. Cette dernière est impraticable car elle demande des caches avec un grand nombre de ports et la présence de plusieurs files d'instructions, qui sont utilisées assez rarement. [[File:Exécution stricte 01.png|centre|vignette|upright=2|Exécution stricte, seconde.]] ====Les processeurs à exécution de chemins multiples==== L'idée précédente peut en théorie être améliorée, afin de non seulement charger les instructions en provenance des deux chemins (celui du branchement pris, et celui du branchement non pris), mais aussi de les exécuter : c'est ce qu'on appelle l''''exécution stricte''' (''eager execution''). Bien sûr, on n’est pas limité à un seul branchement, mais on peut poursuivre un peu plus loin. Quelques papiers de recherche ont étudié l'idée, mais ses défauts font qu'elle n'a jamais été utilisée dans un processeur en dehors de prototypes destinés à la recherche. Le gros problème de l'exécution stricte est qu'on est limité par le nombre d'unités de calculs, de registres, etc. Autant ce serait une technique idéale sur des processeurs avec un nombre illimité de registres ou d'unités de calcul, autant ce n'est pas le cas dans le monde réel. Au bout d'un certain nombre d’embranchements, le processeur finit par ne plus pouvoir poursuivre l’exécution, par manque de ressources matérielles et doit soit stopper, soit recourir à la prédiction de branchement. Il y a le même problème avec le préchargement interne simple, quand on utilise le préchargement du mauvais chemin, comme vu juste au-dessus. ===L'implémentation matérielle du préchargement de cache L2-L1i=== Pour comprendre comment s'effectue le préchargement L2-L1i, il faut regarder comment l'unité de chargement communique avec les caches. L'unité de prédiction de branchement est généralement regroupée avec le ''program counter'' et les circuits associés (les incrémenteurs/MUX associés), pour former l'unité de chargement proprement dite. L'unité de chargement émet des adresses consommées par le cache d'instruction, qui lui-même envoie les instructions lues dans le registre d'instruction ou la file d'instructions. Le couplage de ces structures fait qu'au moindre défaut de cache d'instruction, l'ensemble stoppe. Et notamment, l'unité de prédiction de branchement stoppe en cas de défaut de cache. Même chose si jamais une instruction multicycle s’exécute dans le pipeline et bloque toutes les étapes précédentes. Les pertes de performance ne sont pas très importantes, mais elles existent. Et le préchargement se manifeste dans ces situations. Le préchargement d'instructions consiste à découpler ces structures de manière à ce qu'elles fonctionnent plus ou moins indépendamment. Le but est qu'en plus des accès normaux au cache d'instruction, l'unité de chargement envoie des informations au cache L2 ou L1i en avance, pour effectuer le préchargement. L'unité de chargement doit alors prendre de l'avance sur le cache, pour effectuer les accès au cache L2 en avance, tout en maintenant l'état normal pour effectuer les accès normaux. C'est donc plus ou moins l'unité de chargement qui s'occupe du préchargement, ou du moins les deux sont très liées. ====L'anticipation du ''program counter''==== Avec la solution la plus simple, on a une unité de chargement qui s'occupe des accès au cache d'instruction, et une unité de préchargement qui prend de l'avance sur l'unité de chargement, et communique avec le cache L2. La technique la plus basique se base sur un ''Lookahead program counter'', un second ''program counter'' qui ne fonctionne que lors d'un défaut de cache d'instruction. Il est initialisé avec le ''program counter'' lors d'un défaut de cache, puis il est incrémenté à chaque cycle et les branchements sont prédits, ce qui fait qu'il est mis à jour comme si l’exécution du programme se poursuivait, alors que le reste du processeur est mis en attente. La technique initiale utilisait ce second ''program counter'' pour accéder à une table de prédiction, qui associe à chaque valeur du ''program counter'', l'adresse des données chargées par l'instruction associée. Les adresses fournies à chaque cycle par cette table sont alors envoyées aux unités de préchargement pour qu'elles fassent leur travail. La technique permettait donc de précharger des données en cas de défaut de cache, mais pas d'instructions. Il ne s'agissait pas d'une technique de préchargement des instructions, mais de préchargement de données. La technique a ensuite été adaptée pour le chargement des instructions par Chen, Lee et Mudge. Leur idée utilisait deux unités de prédiction de branchements : une couplée à l'unité de chargement, l'autre pour le préchargement. La première utilisait le ''program counter'' normal, l'autre se déclenchait en cas de défaut de cache et utilisait un ''lookahead program counter''. Les adresses générées par le ''lookahead program counter'' étaient envoyée au cache d'instruction, sur un port de lecture séparé. La ligne de cache lue était alors prédécodée pour détecter les branchements, qui étaient prédits, et rebelote. Il est possible d'adapter la méthode pour que les adresses soient accumulées dans une mémoire FIFO, et étaient consommée par le cache d'instruction L2 pour le préchargement si la ligne de cache associée n'était pas dans le cache d’instruction. Les techniques modernes n'utilisent plus de seconde unité de prédiction de branchement, mais conservent un ''lookahead program counter''. Par contre, le BTB dispose de plusieurs ports : un pour la prédiction de branchement normale, l'autre pour le préchargement. L'unité de préchargement et l'unité de chargement accèdent toutes deux au BTB quand elles ont besoin de faire leurs prédictions, en parallèle. Typiquement, le BTB est accédé à chaque cycle pour la prédiction de branchement, à un rythme plus faible pour le préchargement. ====Le ''Fetch Directed Instruction Prefetching''==== Les processeurs modernes semblent utiliser un algorithme connu sous le nom de '''''Fetch Directed Instruction Prefetching'''''. Il utilise les adresses contenues dans la FTQ pour précharger les instructions adéquates du cache L2 vers le cache L1 d'instruction (L1i). L'unité de préchargement est placée en aval de la FTQ, elle lit son contenu, détecte quelles adresses correspondent à des lignes de cache à précharger, et envoie celles-ci au cache L2. Le préchargement du L2 vers le L1i a lieu quand le cache L2 est inutilisé, ou du moins quand il peut accepter une nouvelle lecture (dans le cas d'un cache multiport et/ou pipeliné). [[File:Fetch directed instruction prefetching.png|centre|vignette|upright=2.5|Fetch directed instruction prefetching]] On peut améliorer légèrement le design précédent sur plusieurs points. Pour éviter de polluer le cache L1 avec des lignes de caches préchargées à tort, il est possible d'ajouter un équivalent des ''stream buffer'' vus dans le chapitre sur le préchargement. Il s'agit d'une autre mémoire FIFO qui mémorise les lignes de cache préchargées. Les lignes de cache préchargées ne sont pas placées dans le cache L1i, mais dans cette file d'attente. Lors d'un accès au L1i, la file d'attente est consultée en parallèle. Si l'instruction voulue est dans la file d'attente, elle est lue depuis la file, et la ligne de cache associée est copiée dans le cache L1i. Mais c'est là une possibilité facultative. Un autre point est que l'unité de préchargement doit attendre que le cache L2 puisse accepter une nouvelle lecture pour lancer le préchargement d'une autre ligne de cache. Pour corriger cela, on ajoute une file d'attente entre le cache L2 et l'unité de préchargement, qui est évidemment une mémoire FIFO. Son utilité dépend des temps de lectures du cache L2, ainsi que de la taille de la FTQ. Elle n'est pas toujours nécessaire, certains processeurs ont un cache L2 assez lent pour qu'on ne puisse précharger qu'une seule ligne de cache avant que la FTQ soit complétement vide. Ces deux optimisations sont facultatives, mais elles étaient présentes dans l'article originel qui a proposé la technique. L'unité de préchargement doit détecter quelles sont les adresses de la FTQ qui ne sont pas déjà chargées dans le L1i. En effet, il est inutile de précharger une ligne de cache si celle-ci est déjà dans le cache L1i. L'unité de préchargement doit donc filtrer au mieux les adresses de la FTQ en deux classes : celles qui correspondent à une ligne de cache déjà dans le L1i, celles qui doivent être préchargées. Pour cela, l'unité de préchargement utilise la technique dit du '''''Cache Probe Filtering'''''. L'idée part du principe que le cache d'instruction L1 est multiport. Les ports du cache d'instruction ne sont pas toujours utilisés en même temps et il arrive qu'il y ait un port de lecture de libre. Le CPF utilise alors ce port inutilisé pour vérifier si la prochaine ligne de cache à précharger est dans le cache ou non. Si c'est le cas, on aura un succès de cache : la ligne de cache est oubliée, elle ne sera pas préchargée. Si ce n'est pas le cas on aura un défaut de cache : la ligne sera préchargée. Notez que l'on a pas besoin de lire la ligne en question, juste de vérifier les tags du cache. Dans ce cas, on peut ajouter des signaux de commande spécifiques pour le CPF, qui font une demi-lecture, qui ne vérifie que les tags, mais ne lit pas la donnée. On peut par exemple ajouter un port spécifique pour le CPF, purement en lecture et qui ne permet que de vérifier les tags. Ce port en plus a un cout en circuits plus faible qu'un port de lecture normal, mais ce n'est pas gratuit du tout. ==Le prédécodage d'instructions== La présence d'un cache d'instruction permet l'implémentation de certaines optimisations, dont la plus connue est la technique dite du '''prédécodage'''. Avec elle, lorsque les instructions sont chargées dans le cache d'instruction, elles sont partiellement décodées, grâce à un circuit séparé de l'unité de décodage d'instruction. Le décodage de l'instruction proprement dit est plus court, car une partie du travail est faite en avance, on gagne quelques cycles. [[File:Prédécodage des instructions dans le cache L1.png|centre|vignette|upright=2.5|Prédécodage des instructions dans le cache L1]] Le prédécodage est particulièrement utile avec des instructions de taille variable : il permet de pré-déterminer où commencent/terminent les instructions dans une ligne de cache, indiquer leur taille, etc. Autre possibilité, le prédécodage peut indiquer s'il y a des branchements dans une ligne de cache et où ils se trouvent, ce qui est très utile pour la prédiction de branchement. Pour chaque ligne de cache, le décodage partiel fournit des informations utiles au décodeur d'instruction. Les informations pré-décodées sont soit intégrée dans la ligne de cache, soit mémorisées dans une banque séparée. En clair : une partie de la capacité totale du cache d'instruction est utilisée pour les informations de pré-décodage. Le prédécodage est donc un compromis : un cache d'instruction de plus faible capacité, mais un décodage plus simple. Le pré-décodage est surtout utile pour les instructions qui sont ré-exécutées souvent. Pour les instructions exécutées une seule fois, le gain en performance dépend de l'efficacité du préchargement et d'autres contraintes, mais ce qui est gagné lors du décodage est souvent partiellement perdu lors du prédécodage. Par contre, si une instruction est exécutée plusieurs fois, le pré-décodage est fait une seule fois, alors qu'on a un gain à chaque ré-exécution de l'instruction. ===Les sélecteurs de branchement intégrés au cache L1=== Le pré-décodage peut être utilisé afin de faciliter le travail de la prédiction de branchement. L'idée est d'incorporer une partie de la prédiction de branchement dans le cache L1 d'instruction. Une ligne de cache mémorise alors des informations de prédiction de branchement dans ses bits de contrôle. Les informations en question peuvent être des adresses de destination, ou simplement de quoi déterminer si le branchement est pris ou non. Il s'agit d'une démarche inverse à celle de la ''Fetch Target Queue'', qui découple l'unité de prédiction de branchement du cache, en insérant une mémoire FIFO entre les deux. Là, la démarche est au contraire de fusionner partiellement cache d'instruction et unité de prédiction de branchement. Les premiers processeurs AMD utilisaient cette technique, au moins dans les grandes lignes. Une ligne de cache contient potentiellement plusieurs branchements, dont la position est identifiée par le prédécodage. Pour chaque octet, la ligne de cache associe un bit de contrôle qui indique si un branchement démarre à cet octet, si c'est le premier octet d'un branchement. Le prédécodage peut identifier entre un et plusieurs branchement par ligne de cache, il y a une limite. Le prédécodage n'identifie typiquement que les 3 à 5 premiers branchements, les suivants sont ignorés, faute de place dans les bits de contrôle. Prenons par exemple une ligne de cache de 8 octets, dans laquelle on a 2 branchements de 2 octets chacun. {|class="wikitable" style="text-align:center;" |- ! colspan="16 | Ligne de cache, en octets |- | Instruction || bgcolor="#FFFF00" | Branch 1 || bgcolor="#FFFF00" | Branch 1 || Instruction || bgcolor="#FFFF00" | Branch 2 || bgcolor="#FFFF00" | Branch 2 || Instruction || Instruction |- ! colspan="16 | Bits d'identification des branchements. |- | 0 || 1 || 0 || 0 || 1 || 0 || 0 || 0 |} Il est possible d'améliorer le tout en précisant quel est le type du branchement. Par exemple, on peut distinguer les branchements inconditionnel et conditionnels, ou encore les instruction de retour de fonction. L'intérêt n'est pas évident, mais c'est lié au fait que les branchements inconditionnels sont toujours pris, et que les retour de fonction ont une adresse de destination qui est prédite par une unité de branchement séparée, le ''return adress predictor'', pas par un BTB. Deux bits suffisent pour indiquer : si c'est un branchement conditionnel, inconditionnel, un retour de fonction, ou une instruction qui n'est pas un branchement. {|class="wikitable" style="text-align:center;" |- ! colspan="16 | Ligne de cache, en octets |- | Instruction || bgcolor="#FFFF00" | Saut inconditionnel || bgcolor="#FFFF00" | Saut inconditionnel || Instruction || bgcolor="#A00000" | Branch cond || bgcolor="#A00000" | Branch cond || Instruction || bgcolor="#F0F000" | Retour de fonction |- ! colspan="16 | Bits d'identification des branchements. |- | 00 || 01 || 00 || 00 || 10 || 00 || 00 || 11 |} L'idée est alors d'ajouter, pour chaque branchement détecté, un '''sélecteur de branchement''' qui indique si le branchement est pris ou non. En clair, des informations de prédiction de branchement sont ajoutés à chaque octet de position. Intuitivement, on se dit qu'il y a seulement un bit par branchement, qui indique si le branchement est pris ou non. Les prédictions peuvent venir soit de l'unité de prédiction de branchement, soit provenir du prédécodage. Le prédécodage peut faire de la prédiction statique. Elle peut notamment détecter les branchements inconditionnels et les marquer comme pris. Elle peut aussi détecter les branchements conditionnels et le marquer comme non-pris par défaut. L'unité de prédiction de branchement met à jour les sélecteurs de branchements si besoin, pour les branchements conditionnels. ===L'incorporation du ''Branch Target Buffer'' dans le cache d'instruction=== Une première optimisation permet de se passer de ''Branch Target Buffer''. Pour rappel, celui-ci est un cache qui mémorise, pour chaque branchement, quelle est son adresse de destination. Il peut contenir d'autres informations de prédiction, mais laissons-les de côté pour le moment. L'idée est de déplacer les adresse de destination des branchements dans le cache d'instruction, dans les lignes de cache. Si une ligne de cache contient un branchement, elle mémorise l'adresse de destination de ce branchement, en plus des bits de pré-décodage. En général, les processeurs ne supportent qu'une seule adresse de destination. Si il y a plusieurs branchements dans une ligne de cache, c'est l'adresse de destination du premier branchement pris dans cette ligne de cache qui est mémorisée. Par exemple, l'AMD K5 se passe de ''Branch Target Buffer'' grâce à cela. Il faut cependant remarquer qu'à ce petit jeu, les instructions de retour de fonction sont à part. Leur adresse de destination est souvent donnée par une unité de branchement séparée, le ''return adress predictor'', séparée du ''Branch Target Buffer''. Leurs adresses de destination n'ont pas forcément besoin d'être mémorisées dans les lignes de cache. La technique décrite ici est simple à comprendre. Cependant, les processeurs AMD anciens, d'architecture K6 à K10 n'utilisaient pas cette méthode, mais une variante plus complexe, capable de prédire jusqu'à deux adresses de destination par branchement. A partir de l'architecture K6, le prédécodage déterminait la position des branchements dans les lignes de cache, dans une limite de 4 branchements par ligne de cache. Pour chaque branchement, la ligne de cache mémorisait un sélecteur de branchement, codé sur 2 bits. La valeur des bits indiquait que le branchement n'est pas pris si elle vaut 00, que c'est une instruction de retour de fonction si elle vaut 01, qu'il faut brancher à l'adresse de destination X si elle vaut 10, qu'il faut brancher à l'adresse de destination X si elle vaut 11. Les adresses de destination sont quand à elles mémorisées dans un cache séparé, appelé le ''Branch Target Cache''. Le mécanisme pour adresser ce cache à partir du cache d'instruction n'est pas très détaillé dans la documentation d'AMD. ===Les avantages et inconvénients=== L'avantage de faire ainsi est que la prédiction de branchement est plus rapide. Lire une instruction depuis le cache renvoie non seulement l'instruction lue, mais aussi des informations de prédiction de branchement. L'unité de prédiction de branchement peut alors utiliser ces informations au cycle suivant pour savoir quelle est l'instruction suivante à charger. Un défaut de cette approche est que si le branchement à prédire n'est pas dans le L1 d'instruction, aucune prédiction de branchement ne peut être faite et le préchargement ne peut pas fonctionner. C'est une limitation que n'ont pas les BTB découplées du cache L1 : elles peuvent prédire les adresses de destination et la direction d'un branchement, tant que l'entrée associée est dans le BTB. Et l'entrée peut être conservée, même si l'instruction en question a quitté le cache L1 et qu'elle est dans le L2, le L3 ou même en mémoire RAM. Les prédictions peuvent même servir à précharger les instructions utiles. Sur l'Itanium et l'AMD Opteron, une optimisation assez intéressante permet de conserver les prédictions de branchement lorsque l'un branchement est évincé du cache L1 et se retrouve dans le cache L2. En théorie, les informations de prédiction, présentes dans la ligne de cache, sont perdues lorsque le branchement est évincé. Mais ces processeurs conservent ces prédictions dans un cache séparé, appelé le '''''L2 Branch Cache'''''. ==Le ''Branch Folding'' des CPU PowerPC== Le '''''Branch Folding''''' est une optimisation qui permet d'exécuter les branchements en avance, pendant qu'ils sont encore dans la file d'instruction. Pour cela, une unité spécialisée scanne la file d'instruction pour y trouver les branchements. Quand elle tombe sur un branchement, elle l'exécute en avance et altère le contenu de la file d'instruction pour remplacer les instructions chargées à tord. L'unité en question s'appelle l''''unité de branchements anticipés'''. Pour les branchements inconditionnels, elle peut les "exécuter directement". Elle retire le branchement et le remplace par l'instruction de destination du branchement. Le ''program counter'' est aussi altéré en avance, ce qui fait que les instructions adéquates sont ensuite chargées dans la file d'instruction. Pour les branchements conditionnels, tout dépend de si le processeur implémente la prédiction de branchement ou non. Sans prédiction de branchement, le mieux est de traiter les branchements conditionnels comme non-pris et prier pour que ca fonction. Avec prédiction de branchement, tout dépend de si le branchement est prédit comme pris ou non. Si le branchement est considéré comme pris, il est traité comme un branchement inconditionnel : il est remplacé par l'instruction de destination et le ''program counter'' est altéré. SI le branchement est non-pris, il suffit de ne rien faire. Notez qu'ils faut que l'adresse de destination soit connue à l'avance, ou du moins puisse être calculée facilement (branchements relatifs). Les premiers processeurs PowerPC utilisaient cette optimisation. Ils avaient une file d'instruction de 8 instructions, et l'unité de branchement scannait les 4 premières instructions. L'unité de branchements anticipés contenait les registres nécessaires pour gérer les branchements. Elle contenait précissément le ''link register'' pour des appels de procédures, le ''Count Target Register'' et le ''Count Register'' utilisés pour les boucles. Il contient aussi un additionneur pour calculer les adresses des branchements relatifs. L'unité de branchements anticipés doit identifier les branchements, ce qui demande de décoder partiellement les instructions en attente. Une solution pratique est d'utiliser la technique du pré-décodage d'instruction, de la section précédente. Le pré-décodage est parfait pour identifier les branchements dans un bloc d'instruction. Les informations prédécodées peuvent alors être utilisées pour remplir la file d'instruction. Chaque instruction dans la file d'instruction est associée à un bit qui indique si c'est un branchement ou non. L'unité de branchement anticipé peut alors détecter les branchements avec un simple encodeur à priorité. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=La prédiction de branchement | prevText=La prédiction de branchement | next=Les pipelines multicycles | nextText=Les pipelines multicycles }} </noinclude> {{AutoCat}} j7vwlzjufqf7l9uofqbntkhyt8cqhue 772808 772774 2026-09-22T07:11:35Z DavidL 1746 772808 wikitext text/x-wiki Les processeurs avec un pipeline sont découpés en deux sections : un ''front-end'' qui charge et décode les instructions, et un ou plusieurs ''back-end'' qui exécutent les instructions. L'exécution des micro-opérations est optimisée par des techniques que nous verrons dans la suite du cours : exécution dans le désordre, superscalarité, renommage de registre. Mais un point important est que le ''front-end'' a des optimisations dédiées. La prédiction de branchement est l'une de ces optimisation, mais elle n'est pas la seule. Ce chapitre aborde les optimisations autres que la prédiction de branchement. Nous allons voir le découplage des étages du ''front-end'', le pré-décodage et quelques améliorations liées indirectement à la prédiction de branchement. ==Le découplage du ''front-end''== Le séquenceur d'un processeur contient une unité de chargement, un décodeur et un chemin de données. Avec un pipeline, le couplage de ces structures fait que si l'une d'entre elle prend plus d'un cycle pour faire son travail, les étages précédents et/ou suivants sont stoppés. Par exemple, lors d'un défaut dans le cache d'instruction, l'ensemble stoppe. De même, si jamais le décodeur doit décoder une instruction via le micro-code, cela prend plusieurs cycles : l'unité de chargement et le cache d'instruction sont inutilisés. Même chose si jamais une instruction multicycle s’exécute dans le pipeline : cela bloque toutes les étapes précédentes. Mais il s'agit là d'un défaut inhérent aux pipelines basiques, qui relient chaque étage avec des registres. Avec un peu d'astuce, il est possible que certains étages prennent de l'avance même si l'étage suivant est bloqué. Par exemple, si le décodeur bloque le pipeline en utilisant son micro-code pendant 4-5 cycles, l'unité de chargement peut en théorie précharger à l'avance les instructions suivantes, et les mettre en attente. Ou encore, en cas de bulle de pipeline (''pipeline stall''), le décodeur peut décoder en avance des instructions et les mettre en attente tant que la bulle de pipeline est en cours. Dans les deux cas, une unité prend de l'avance et met en attente ses résultats tant que l'étage suivant est occupé. La mise en attente est réalisée en remplaçant/complémentant les registres du pipeline avec des mémoires FIFOs. Elles remplacent ou complémentant les registres de pipeline, les deux sont possibles. Si elles remplacent ces registres, si aucune mise en attente n'est requise, elles fonctionnent comme un registre de pipeline. ===La file d'instruction=== [[File:File de micro-opérations.png|vignette|upright=1|File d'instructions]] Les processeurs modernes intègrent une '''file d'instruction''', à savoir une mémoire FIFO placée entre le cache d'instruction et le décodeur d'instruction. Les instructions chargées par l'étape de chargement soient accumulées dans la file d'instructions et sont décodées quand l'unité de décodage est prête. La file d'attente permet de charger des instructions à l'avance, permettant ainsi de masquer certains accès au cache ou à la mémoire assez longs. L'idée est que les instructions s'accumulent dans la file d'instruction si le processeur exécute les instructions moins vite qu'il ne les charge. C'est généralement signe qu'il effectue une instruction multicycle et/ou qu'il effectue un accès à la mémoire. À l'inverse, la file d'attente se vide quand le processeur exécute les instructions plus vite qu'il n'en charge. C'est généralement signe qu'un défaut de cache d'instruction est en cours. La présence d'une file d'attente fait que la première situation est compensée lors de la seconde. Les temps d'attentes liées aux instructions multicycles permettent de remplir la file d'attente, qui est ensuite vidée en cas de défaut de cache. Le processeur exécute en permanence des instructions, sans interruption. Alors que sans file d'attente, les défauts de cache entraineront des temps d'attente où le processeur s’exécuterait rien. La seule limite de cette optimisation est l'influence des branchements. Lorsqu'un branchement est décodé, ce tampon d’instructions est totalement vidé de son contenu. Ce n'est ni plus ni moins ce que faisait la ''prefetch input queue'' des anciens processeurs Intel, dont nous avions parlé dans le chapitre sur l'unité de chargement et le séquenceur. ===La file de micro-opérations=== [[File:File de micro-opération.png|vignette|upright=1|File d'instruction]] L'optimisation précédente peut aussi s'appliquer entre le décodeur d'instruction et le chemin de données. Pour cela, la sortie du décodeur est reliée à une mémoire FIFO semblable à la file d'instruction. Elle mémorise les micro-opérations émises par le décodeur et les met en attente tant que le reste du pipeline n'est pas prêt. Nous l'appellerons la '''file de micro-opérations''', mais elle porte de nombreux noms et la terminologie des différents fabricants est assez confuse. Le schéma ci-contre indique que la file de micro-opérations est située en sortie de l’unité de décodage, avant l'unité d'émission et avant l'unité de renommage de registres (que nous aborderons dans quelques chapitres). La file de micro-opérations permet aux décodeurs de faire leur travail même si le reste du pipeline n'est pas prêt. Par exemple, imaginons que le processeur ne peut pas émettre de nouvelle instruction, soit car toutes les ALUs sont occupées, soit car il y a un accès mémoire qui bloque le pipeline, peu importe. Sans file de micro-opérations, tout ce qui précède l'unité d'émission devrait être totalement bloqué tant que l'instruction ne peut pas être émise. Mais avec une file de micro-opérations, le pipeline peut charger et décoder des instructions, il a juste à accumuler les instructions décodées dans la file de micro-opérations. En clair, la file de micro-opérations met en attente les instructions décodées quand des bulles de pipeline sont émises. Et à l'inverse, elle permet d'émettre des instructions quand les unités de décodage/chargement sont bloquées. Le cas classique est celui d'un défaut de cache dans le cache d'instruction. Pendant un défaut de cache, aucune instruction n'est chargée ni décodées durant quelques cycles. Sans file de micro-opérations, le processeur ne peut plus rien faire durant quelques cycles. Mais avec une file de micro-opérations, il se rattrape en émettant les instructions en attente dans la file de micro-opérations, s'il y en a. En clair, si l'unité d'émission a mis en attente des instructions, le processeur se rattrape au prochain défaut de cache d'instruction. Une autre situation où le décodeur bloque est le cas où certaines instructions mettent du temps à être décodées. C'est notamment le cas de certaines instructions complexes, dont le décodage prend facilement 2 à 3 cycles d'horloge, voire plus. Le pire est le décodage des instructions microcodées, qui peut demander plusieurs cycles. Or, le pipeline demande qu'on décode une instruction par cycle pour éviter de bloquer le pipeline. Mais ce temps de décodage peut être masqué si des micro-opérations sont en attente dans la file, elles sont exécutées pendant le décodage long. ===Le ''Loop Stream Detector''=== Les boucles sont une opportunité d'optimisation très intéressante sur les CPU avec une file de micro-opérations. L'idée est que lors d'une boucle, des instructions sont chargées, décodées et exécutées plusieurs fois de suite. Mais à chaque répétition d'une instruction, le chargement et le décodage donnent toujours le même résultat, seule l'exécution n'est pas la même (les registres renommés sont aussi différents, mais nous verrons cela dans plusieurs chapitres). L'idée est simplement de mémoriser les N dernières instructions décodées et de les ré-exécuter si besoin. Ainsi, on évite de charger/décoder une même instruction machine plusieurs fois, mais de réutiliser les micro-opérations déjà décodées. L'implémentation la plus simple utiliser la file de micro-opérations comme une sorte de pseudo-cache FIFO. La file de micro-opérations ne supprime pas les micro-opérations une fois qu'elles sont émises. Elle mémorise là où se trouve la dernière micro-opération émise, mais conserve celles qui ont déjà été émises. Un circuit annexe, appelé le '''''Loop Stream Detector''''' (LSD), détecte les boucles dans la file de micro-opérations et optimise leur exécution. Si une boucle adéquate est détectée par le ''Loop Stream Detector'', les micro-opérations de la boucle sont lues dans la file de micro-opération et sont injectées directement dans la suite du pipeline. De plus, les unités de chargement et de décodage sont désactivées pendant l’exécution de la boucle, ce qui réduit la consommation d'énergie du CPU. L'optimisation accélère les petites boucles, qui tiennent toutes entières dans la file de micro-opérations, et sous condition qu'elles s'exécutent de la même manière à chaque exécution. De telles boucles exécutent une suite de N instructions, qui reste identique à chaque itération de la boucle. Le cas le plus simple est celui d'une boucle dans laquelle il n'y a pas de branchements. Pour les boucles normales, le processeur reprend une exécution normale quand on quitte la boucle ou quand son exécution change, par exemple quand un if...else, un return ou tout autre changement de flot de contrôle a lieu. Vu que toutes ces situations impliquent un branchement qui n'a pas été pris comme avant, le processeur n'utilise plus le ''Loop Stream Detector'' en cas de mauvaise prédiction de branchement. Le LSD vise surtout à désactiver les décodeurs et l'unité de chargement lors de l'exécution d'une boucle. La désactivation peut être du ''clock gating'', voire du ''power gating'', être partielle ou totale. Dans le pire des cas, les unités de chargement peuvent continuer à charger des instructions en avance dans une file d'instruction, mais les décodeurs peuvent être désactivés. Dans le meilleur des cas, la totalité de ce qui précède la file de micro-opération est désactivé tant que la boucle s’exécute normalement. Y compris le cache de micro-opération. [[File:Loop Stream Detector.png|centre|vignette|upright=2|Loop Stream Detector]] Évidemment, la taille des boucles optimisées ainsi est limitée par la taille de la file de micro-opération, ce qui fait que l'optimisation ne fonctionne que pour des boucles de petite taille. Pour donner quelques chiffres, les processeurs ARM Cortex A15 géraient des boucles de maximum 32 micro-opérations. De plus, toute la file de micro-opération n'est pas gérée par le ''loop stream detector''. Par exemple, les processeurs avec une file de micro-opération de 64 micro-opération peuvent gérer des boucles de maximum 32 à 40 micro-opérations. Mais les contraintes principales portent sur la détection des boucles. Le ''Loop Stream Detector'' ne peut pas détecter toutes les boucles qui existent, et certaines boucles ne sont pas détectées. Par exemple, le ''Loop Stream Detector' ne peut pas détecter les boucles si un appel de fonction a lieu dans la boucle. Il y a aussi des contraintes quant au nombre de branchements à l'intérieur de la boucle et le nombre d'accès mémoire. Les CPU Intel modernes disposent d'un ''loop stream detector'', les CPU AMD en avaient sur les micro-architectures Zen 4 mais il a disparu sur la micro-architecture Zen 5. Quelques CPU ARM avaient aussi un ''loop stream detector'', notamment le Cortex A15. Il faut noter que le ''loop stream detector'' a été désactivé par des mises à jour de microcode sur quelques architectures, comme sur la micro-architecture Zen 4 d'AMD ou les CPU de micro-architecture Skylake et Kaby Lake d'Intel. Pour la micro-architecture Skylake, les raisons officielles pour cette désactivation sont un bug lié à l'interaction avec l'''hyperthreading''. Il est vraisemblable que des bugs ou des problèmes de sécurité aient amené à la désactivation sur les autres architectures. ===Le cache de micro-opérations=== Le '''cache de micro-opérations''' a le même but que le ''Loop Stream Detector'', à savoir optimiser l'exécution des boucles. La différence avec le ''Loop Stream Detector'' est qu'il y a un cache séparé de la file de micro-opérations, qui mémorise des micro-opérations décodées, dans le cas où elles soient réutilisées par la suite. La première itération d'une boucle accumule les instructions décodées dans le cache de micro-opérations, les itérations suivantes de la boucle lisent les micro-opérations adéquates dans le cache de micro-opération : on n'a pas à décoder l'instruction une nouvelle fois. Sur de nombreux processeurs, le cache de micro-opération est alimenté non pas par l'unité de décodage, mais par la file de micro-opérations. Ainsi, seules les micro-opérations émises sont copiées dans ce cache. [[File:File de micro-opérations et cache de micro-ops.png|centre|vignette|upright=2|File de micro-opérations et cache de micro-ops]] Les avantages sont les mêmes qu'avec un ''Loop Stream Detector'' : une consommation énergétique réduite, des performances légèrement améliorées. Le décodeur et l'unité de chargement sont inutiles en cas de succès dans le cache de micro-opération, ce qui fait qu'ils sont désactivés, éteints, ou du moins subissent un ''clock-gating'' temporaire. Ils ne consomment pas d'énergie, seul le cache de micro-opération utilise de l'électricité. L'avantage en termes de performance est plus faible, assez variable suivant la situation, mais aussi bien le cache de micro-opérations que le LSD ne font pas de mal. Une différence avec le cache de micro-opération est qu'une boucle doit s’exécuter à l'identique avec un ''Loop Stream Detector'', pas avec un cache de micro-opérations. Prenons l'exemple d'une boucle contenant quelques instructions suivies par un IF...ELSE. Il arrive qu'une itération de la boucle exécute le IF, alors que d'autres exécutent le ELSE. Dans ce cas, le ''Loop Stream Detector'' ne sera pas activé, car la boucle ne s’exécute pas pareil d'une itération à l'autre. Par contre, un cache de macro/micro-opération mémorisera les micro-opérations du IF et du ELSE et les fournira selon les besoins. La raison est que le cache de micro-opérations a une politique de remplacement des lignes de cache plus complexe que le FIFO, typiquement une politique LRU ou LFU approximée. Le cache de micro-opération est donc plus efficace que le ''Loop Stream Detector'', pour un cout en transistor plus élevé. Le cache de micro-opération est une voie de chargement parallèle au ''front-end'' proprement dit. L'accès au cache de micro-opération se fait lors de l'étape de chargement. Le cache de micro-opérations est adressé en envoyant le ''program counter'' sur son entrée d'adresse, en parallèle du cache d'instruction. En clair, il y a une voie qui regroupe cache d'instruction, file d'instruction et décodeur, et une seconde voie qui se résume au cache de micro-opération. Les deux voies sont accédées en parallèle. En cas de succès dans le cache de micro-opération, les micro-opérations adéquates sont lues directement depuis le cache de micro-opération. Le cache de micro-opération associe, pour chaque instruction machine, une ou plusieurs micro-opérations. Avec l'implémentation la plus simple, une ligne de cache est associée à une instruction machine. Par exemple, sur les processeurs Intel de micro-architecture Skylake, chaque ligne de cache était associée à une instruction machine et pouvait contenir de 1 à 6 micro-opérations. Une instruction devait tenir toute entière dans une ligne de cache, ce qui fait que les instructions décodées en plus de 6 micro-opérations ne pouvaient pas rentrer dans ce cache. Il existe deux méthodes différentes pour encoder les micro-opérations dans le cache de micro-opérations. La première est la plus intuitive : on mémorise les micro-opérations dans la ligne de cache, directement. Elle est utilisée sur les processeurs AMD, et sans doute sur les processeurs Intel récents. Mais les anciens processeurs Intel, comme ceux des architectures Sandy Bridge et Netburst, utilisent une autre méthode. Une ligne de cache mémorise non pas les micro-opération directement, mais un pointeur vers le ''control store'', qui indique à quelle adresse dans le micro-code se situe la micro-opération. La micro-opération est donc lue depuis le micro-code lors de son envoi au chemin de données, lors de son émission. Il faut noter que pour des raisons de performance, le cache de micro-opérations est virtuellement tagué, ce qui fait qu'il est invalidé en cas de changement de programme. Sur l'architecture Sandy Bridge, il est carrément inclus dans le cache L1, les deux sont des caches inclusifs l'un avec l'autre. Les premières implémentations étaient très limitées. Les micro-opérations devaient être séquentielles dans le code, le cache était consulté seulement après un branchement et non à chaque instruction, pour limiter la consommation d'énergie an détriment des performances. Ces limitations ne sont pas présentes sur les architectures récentes. Le cache de micro-opérations et le ''Loop Stream Detector'' font la même chose, mais certains processeurs implémentaient les deux. L'avantage est que le cache de micro-opération peut être désactivé si jamais le LSD détecte une boucle dans la file d'instruction, ce qui réduit encore plus la consommation énergétique. En pratique, l'impact sur la consommation énergétique est très difficile à mesurer, mais il rajoute de la complexité pour la conception du processeur. [[File:File de micro-opérations et cache de micro-ops - Copie.png|centre|vignette|upright=2.5|File de micro-opérations et cache de micro-ops - Copie]] ==Le préchargement d'instructions et la ''Fetch Target Queue''== Nous venons de voir qu'il est possible de découpler les étages de chargement, décodage et le chemin de données, en insérant des mémoires FIFOs dans le pipeline. Il en est de même avec l'unité de chargement elle-même. Elle est composée de deux circuits entre lesquels on peut ajouter des mémoires FIFO : une unité de calcul d'adresse et le cache d'instruction. L'unité de calcul d'adresse regroupe : l'unité de prédiction de branchement, le ''program counter'', le circuit pour incrémenter le ''program counter'', les MUX associés pour gérer les branchements. L'unité de calcul d'adresse émet les adresses des instructions à charger, qui sont consommées par le cache d'instruction. Les processeurs modernes incorporent une optimisation assez intéressante : ils découplent l'unité de calcul d'adresse de l'accès au cache d'instruction. Pour cela, ils incorporent une mémoire FIFO entre l'unité de prédiction de branchement et le cache d'instruction. Les premiers articles scientifiques, qui ont proposé cette solution, l'ont appelée la '''''Fetch Target Queue''''', abréviée FTQ. Elle accumule les adresses à lire/écrire dans le cache d'instruction, peu importe que ces adresses viennent du ''program counter'' ou de l'unité de prédiction de branchement. [[File:Fetch target queue.png|centre|vignette|upright=2.5|Fetch target queue]] Elle se remplit quand le cache d'instruction est bloqué, soit à cause d'un défaut de cache, soit à cause d'un pipeline bloqué en amont de l'unité de chargement. Par exemple, si le cache d'instruction est bloqué par un défaut de cache, l'unité de prédiction de branchement peut accumuler des prédictions à l'avance dans la FTQ, qui sont ensuite consommées par le cache d'instruction une fois qu'il est redevenu disponible. De même, si l'unité de prédiction de branchement est bloquée par un évènement quelconque, le cache d'instruction peut consommer les prédictions faites à l'avance. Une utilisation assez originale de la FTQ s'est vu sur les processeurs AMD d'architectures bulldozer. Sur cette architecture, les cœurs étaient regroupés par paquets de deux, et les deux cœurs partageaient certains circuits. Notamment, l'unité de prédiction de branchement était partagée entre les deux cœurs ! Pourtant, chaque cœur disposait de sa propre FTQ ! Un avantage de la FTQ tient dans le fait que les caches d'instructions sont pipelinés, sur le même modèle que les processeurs. On peut leur envoyer une demande de lecture/écriture par cycle, alors que chaque lecture/écriture prendra plusieurs cycles à s'effectuer. L'accès au cache d'instruction a donc une certaine latence, qui est partiellement masquée par la FTQ au point où elle ne s'exprime qu'en cas de défaut de cache assez important. Par exemple, si l'accès au cache d'instruction prend 4 cycles, une FTQ qui met en attente 4 adresses camouflera le temps d'accès au cache, tant qu'il n'y a pas de mauvaise prédiction de branchement. La FTQ est aussi très utile avec les unités de branchement modernes, qui peuvent mettre plusieurs cycles pour fournir une prédiction. Prendre de l'avance avec une FTQ amorti partiellement le temps de calcul des prédictions. : Si le cache d'instruction est multiport et accepte plusieurs accès simultanés, il peut consommer plusieurs entrées dans la FTQ à la fois. Mais l'avantage principal de la FTQ est qu'elle permet l'implémentation d'une optimisation très importante. Il y a quelques chapitres, nous avions parlé des techniques de '''préchargement d'instruction''', qui permettent de charger à l'avance des instructions dans le cache d'instruction. Nous avions volontairement laissé de côté le préchargement des instructions, pour tout un tas de raisons. Et la raison est justement que la prédiction de branchement et le préchargement des instructions sont fortement liés sur les processeurs modernes. Il est maintenant possible d'aborder le préchargement pour les instructions, d’où cette section. Notons que par préchargement des instructions, on peut parler de deux formes de préchargement, fortement différentes. La première correspond au préchargement normal, à savoir le préchargement des instructions dans le cache d'instruction L1, à partir du cache L2. Il s'agit donc d'un préchargement dans le cache d'instruction. Mais il existe aussi une autre forme de préchargement, qui consiste à précharger à l'avance des instructions dans la file d'instruction et qui a été abordée dans la section sur la ''prefetch input queue''. Les deux formes de préchargement n'ont pas lieu au même endroit dans la hiérarchie mémoire : l'une précharge du cache L2 vers le L1i, l'autre du cache L1i vers la file d'instruction (ou dans le cache de macro-opération). Mais les algorithmes utilisés pour sont sensiblement les mêmes. Aussi, nous allons les voir en même temps. Pour faire la distinction, nous parlerons de préchargement L2-L1i pour la première, de préchargement interne pour l'autre. ===Les algorithmes de préchargement d'instructions=== Les techniques basiques de préchargement consistent à charger des instructions qui suivent la dernière ligne de cache accédée. Quand on charge des instructions dans le cache d’instruction, les instructions qui suivent sont chargées automatiquement, ligne de cache par ligne de cache. il s'agit due préchargement séquentiel, la technique la plus simple de préchargement, qui profite de la localité spatiale. Elle est utilisée pour précharger des instructions du cache L2 vers le cache L1i, mais aussi pour le préchargement interne dans la file d'instructions. [[File:Branchements et préchargement séquentiel.png|centre|vignette|upright=2|Branchements et préchargement séquentiel.]] Mais un ''prefetcher'' purement séquentiel gère mal les branchements. Si un branchement est pris, les instructions de destination ne sont pas chargées, si elles ne sont pas dans la ligne de cache suivante. Pour le préchargement L2-L1i, cela ne pose pas de problèmes majeurs, au-delà de la pollution du cache L1i par des instructions inutiles. Mais pour le préchargement interne, c'est autre chose. Les instructions préchargées par erreurs doivent être supprimées pour éviter qu'elles soient décodées et exécutées, ce qui fait que la file d’instruction doit être invalidée. Il existe des techniques de préchargement plus élaborées qui marchent mieux en présence de branchements. Elles utilisent toutes une collaboration de l'unité de prédiction de branchement. Elles accèdent au ''Branch Target Buffer'', pour détecter les branchements, leur destination, etc. Le tout peut se coupler à la technique du prédécodage. Avec cette dernière, le prédécodage décode en partie les instructions lors de leur chargement dans le cache, et détecte les branchements et leur adresse de destination à ce moment-là. Ces informations sont alors mémorisées dans une table à part, ou dans le BTB. Mais la plupart des designs utilisent le BTB, par souci de simplicité. Il existe globalement deux à trois techniques principales, que nous allons voir dans ce qui suit. La première technique prédit si le branchement est pris ou non, et agit différemment si le branchement est pris ou non. Si le branchement est pris, elle précharge les instructions à partir de l'adresse de destination des branchements pris. Sinon, elle précharge les instructions suivantes avec préchargement séquentiel. Il s'agit du '''''target line prefetching''''' [[File:Target line prefetching.png|centre|vignette|upright=2|Target line prefetching.]] Une autre technique ne prédit pas les branchements et précharge à la fois les instructions suivantes avec le ''next-line prefetching'', et la ligne de cache de destination du branchement avec le ''target line prefetching''. Comme ça, peu importe que le branchement soit pris ou non, les instructions adéquates seront préchargées quand même. On appelle cette technique le '''préchargement du mauvais chemin''' (''wrong path prefetching''). [[File:Préchargement du mauvais chemin.png|centre|vignette|upright=2|Préchargement du mauvais chemin.]] Le ''target line prefetching'' est plus complexe à implémenter, car il demande de prédire les branchements. Mais elle a l'avantage de ne pas précharger inutilement deux lignes de cache par branchement, seulement une seule. Par contre, le préchargement est inutile en cas de mauvaise prédiction de branchement : non seulement on a préchargé une ligne de cache inutilement, mais en plus, la ligne de cache adéquate n'a pas été chargée. On n'a pas ce problème avec le préchargement du mauvais chemin, qui garantit que la ligne de cache adéquate est toujours préchargée. ===L'implémentation du préchargement interne, dans la file d'instruction=== Le préchargement dans la file d'instruction est généralement de type séquentiel, mais certains processeurs font autrement. Déjà, il faut remarquer que le ''target line prefetching'' correspond en réalité à la prédiction de branchement classique. L'adresse de destination est prédite, et on charge les instructions adéquates dans la file d'instruction. La prédiction de branchement, associée à une file d'instruction, est donc une forme de préchargement. Il fallait y penser. Enfin, des processeurs assez rares utilisaient le préchargement du mauvais chemin. Le préchargement du mauvais chemin demande d'utiliser deux files d'instructions séparées. L'une dans laquelle on précharge de manière séquentielle, l'autre dans laquelle on utilise la prédiction de branchement pour faire du ''target line prefetching''. Une fois que l'on sait si la prédiction de branchement était correcte, on est certain qu'une des deux files contiendra les instructions valides. Le contenu de la file adéquate est conservé, alors que l'autre est intégralement invalidée. Le choix de la bonne file se fait avec un multiplexeur. C'est approximativement la technique qui était implémentée sur le processeur de mainframe IBM 370/165, par exemple, et sur quelques modèles IBM similaires. Le problème est que cette méthode demande de charger deux instructions à chaque cycle. Cela demande donc d'utiliser un cache d'instruction multiport, avec un port par file d'instruction. Le cout en circuit d'un cache double port n'est pas négligeable. Et le gain en performance est assez faible. Le préchargement dans la file d’instruction permet d'économiser quelques cycles lors de l'accès au cache d'instruction, guère plus. Le gain est maximal lorsque les instructions préchargées ont généré un défaut de cache, qui a rapatrié les instructions adéquates pendant que le processeur exécutait les mauvaises instructions, avant que la mauvaise prédiction de branchement soit détectée. Dans ce cas, le défaut de cache a eu lieu pendant la mauvaise prédiction et sa réparation, et non après. ====La gestion des branchements successifs==== Un autre défaut de cette méthode est la présence de branchements successifs. Par exemple, si jamais on rencontre un branchement, le flux d'instructions se scinde en deux : un où le branchement est pris, un autre où il ne l'est pas. Chacun de ces flux peut lui-même contenir un branchement, et se scinder lui aussi. Et ainsi de suite. Et le processeur doit gérer cette situation en termes de préchargement. [[File:Exécution stricte 04.png|centre|vignette|upright=2|Exécution stricte]] Plusieurs solutions existent. La méthode la plus simple stoppe le chargement du flux en attendant que le premier branchement soit terminé. Cette solution est intuitive, mais est celle où on a les gains en performance les plus faibles. Elle est couramment implémentée d'une manière assez particulière, qui ne correspond pas tout à fait à un stop du chargement, mais qui utilise les lignes de cache. L'unité de préchargement est conçue pour copier des lignes de cache entières dans la file d'instruction. Le processeur (pré-)charge deux lignes de cache : celle du bon chemin, celle du mauvais chemin. Il les précharge dans deux files d'instructions, qui contiennent généralement une ligne de cache grand maximum. Le temps que l'on ait chargé les deux files d'instruction, le résultat du branchement est connu et on sait laquelle est la bonne. L'autre possibilité est d'utiliser la prédiction de branchement pour ce flux, afin de poursuivre le chargement de manière spéculative. Elle donne de bonnes performances, mais demande des unités de prédiction de branchement spéciales, dans le cas où les deux flux tombent sur un branchement en même temps. Cette technique est indirectement liée au cache de traces que nous verrons dans le chapitre sur les processeurs superscalaires. Nous n'en parlons pas ici, car ce genre de techniques est plus liée aux processeurs superscalaires qu'un processeur avec un pipeline normal. Une autre possibilité consiste à scinder ce flux en deux et charger les deux sous-flux. Cette dernière est impraticable car elle demande des caches avec un grand nombre de ports et la présence de plusieurs files d'instructions, qui sont utilisées assez rarement. [[File:Exécution stricte 01.png|centre|vignette|upright=2|Exécution stricte, seconde.]] ====Les processeurs à exécution de chemins multiples==== L'idée précédente peut en théorie être améliorée, afin de non seulement charger les instructions en provenance des deux chemins (celui du branchement pris, et celui du branchement non pris), mais aussi de les exécuter : c'est ce qu'on appelle l''''exécution stricte''' (''eager execution''). Bien sûr, on n’est pas limité à un seul branchement, mais on peut poursuivre un peu plus loin. Quelques papiers de recherche ont étudié l'idée, mais ses défauts font qu'elle n'a jamais été utilisée dans un processeur en dehors de prototypes destinés à la recherche. Le gros problème de l'exécution stricte est qu'on est limité par le nombre d'unités de calculs, de registres, etc. Autant ce serait une technique idéale sur des processeurs avec un nombre illimité de registres ou d'unités de calcul, autant ce n'est pas le cas dans le monde réel. Au bout d'un certain nombre d’embranchements, le processeur finit par ne plus pouvoir poursuivre l’exécution, par manque de ressources matérielles et doit soit stopper, soit recourir à la prédiction de branchement. Il y a le même problème avec le préchargement interne simple, quand on utilise le préchargement du mauvais chemin, comme vu juste au-dessus. ===L'implémentation matérielle du préchargement de cache L2-L1i=== Pour comprendre comment s'effectue le préchargement L2-L1i, il faut regarder comment l'unité de chargement communique avec les caches. L'unité de prédiction de branchement est généralement regroupée avec le ''program counter'' et les circuits associés (les incrémenteurs/MUX associés), pour former l'unité de chargement proprement dite. L'unité de chargement émet des adresses consommées par le cache d'instruction, qui lui-même envoie les instructions lues dans le registre d'instruction ou la file d'instructions. Le couplage de ces structures fait qu'au moindre défaut de cache d'instruction, l'ensemble stoppe. Et notamment, l'unité de prédiction de branchement stoppe en cas de défaut de cache. Même chose si jamais une instruction multicycle s’exécute dans le pipeline et bloque toutes les étapes précédentes. Les pertes de performance ne sont pas très importantes, mais elles existent. Et le préchargement se manifeste dans ces situations. Le préchargement d'instructions consiste à découpler ces structures de manière à ce qu'elles fonctionnent plus ou moins indépendamment. Le but est qu'en plus des accès normaux au cache d'instruction, l'unité de chargement envoie des informations au cache L2 ou L1i en avance, pour effectuer le préchargement. L'unité de chargement doit alors prendre de l'avance sur le cache, pour effectuer les accès au cache L2 en avance, tout en maintenant l'état normal pour effectuer les accès normaux. C'est donc plus ou moins l'unité de chargement qui s'occupe du préchargement, ou du moins les deux sont très liées. ====L'anticipation du ''program counter''==== Avec la solution la plus simple, on a une unité de chargement qui s'occupe des accès au cache d'instruction, et une unité de préchargement qui prend de l'avance sur l'unité de chargement, et communique avec le cache L2. La technique la plus basique se base sur un ''Lookahead program counter'', un second ''program counter'' qui ne fonctionne que lors d'un défaut de cache d'instruction. Il est initialisé avec le ''program counter'' lors d'un défaut de cache, puis il est incrémenté à chaque cycle et les branchements sont prédits, ce qui fait qu'il est mis à jour comme si l’exécution du programme se poursuivait, alors que le reste du processeur est mis en attente. La technique initiale utilisait ce second ''program counter'' pour accéder à une table de prédiction, qui associe à chaque valeur du ''program counter'', l'adresse des données chargées par l'instruction associée. Les adresses fournies à chaque cycle par cette table sont alors envoyées aux unités de préchargement pour qu'elles fassent leur travail. La technique permettait donc de précharger des données en cas de défaut de cache, mais pas d'instructions. Il ne s'agissait pas d'une technique de préchargement des instructions, mais de préchargement de données. La technique a ensuite été adaptée pour le chargement des instructions par Chen, Lee et Mudge. Leur idée utilisait deux unités de prédiction de branchements : une couplée à l'unité de chargement, l'autre pour le préchargement. La première utilisait le ''program counter'' normal, l'autre se déclenchait en cas de défaut de cache et utilisait un ''lookahead program counter''. Les adresses générées par le ''lookahead program counter'' étaient envoyée au cache d'instruction, sur un port de lecture séparé. La ligne de cache lue était alors prédécodée pour détecter les branchements, qui étaient prédits, et rebelote. Il est possible d'adapter la méthode pour que les adresses soient accumulées dans une mémoire FIFO, et étaient consommée par le cache d'instruction L2 pour le préchargement si la ligne de cache associée n'était pas dans le cache d’instruction. Les techniques modernes n'utilisent plus de seconde unité de prédiction de branchement, mais conservent un ''lookahead program counter''. Par contre, le BTB dispose de plusieurs ports : un pour la prédiction de branchement normale, l'autre pour le préchargement. L'unité de préchargement et l'unité de chargement accèdent toutes deux au BTB quand elles ont besoin de faire leurs prédictions, en parallèle. Typiquement, le BTB est accédé à chaque cycle pour la prédiction de branchement, à un rythme plus faible pour le préchargement. ====Le ''Fetch Directed Instruction Prefetching''==== Les processeurs modernes semblent utiliser un algorithme connu sous le nom de '''''Fetch Directed Instruction Prefetching'''''. Il utilise les adresses contenues dans la FTQ pour précharger les instructions adéquates du cache L2 vers le cache L1 d'instruction (L1i). L'unité de préchargement est placée en aval de la FTQ, elle lit son contenu, détecte quelles adresses correspondent à des lignes de cache à précharger, et envoie celles-ci au cache L2. Le préchargement du L2 vers le L1i a lieu quand le cache L2 est inutilisé, ou du moins quand il peut accepter une nouvelle lecture (dans le cas d'un cache multiport et/ou pipeliné). [[File:Fetch directed instruction prefetching.png|centre|vignette|upright=2.5|Fetch directed instruction prefetching]] On peut améliorer légèrement le design précédent sur plusieurs points. Pour éviter de polluer le cache L1 avec des lignes de caches préchargées à tort, il est possible d'ajouter un équivalent des ''stream buffer'' vus dans le chapitre sur le préchargement. Il s'agit d'une autre mémoire FIFO qui mémorise les lignes de cache préchargées. Les lignes de cache préchargées ne sont pas placées dans le cache L1i, mais dans cette file d'attente. Lors d'un accès au L1i, la file d'attente est consultée en parallèle. Si l'instruction voulue est dans la file d'attente, elle est lue depuis la file, et la ligne de cache associée est copiée dans le cache L1i. Mais c'est là une possibilité facultative. Un autre point est que l'unité de préchargement doit attendre que le cache L2 puisse accepter une nouvelle lecture pour lancer le préchargement d'une autre ligne de cache. Pour corriger cela, on ajoute une file d'attente entre le cache L2 et l'unité de préchargement, qui est évidemment une mémoire FIFO. Son utilité dépend des temps de lectures du cache L2, ainsi que de la taille de la FTQ. Elle n'est pas toujours nécessaire, certains processeurs ont un cache L2 assez lent pour qu'on ne puisse précharger qu'une seule ligne de cache avant que la FTQ soit complétement vide. Ces deux optimisations sont facultatives, mais elles étaient présentes dans l'article originel qui a proposé la technique. L'unité de préchargement doit détecter quelles sont les adresses de la FTQ qui ne sont pas déjà chargées dans le L1i. En effet, il est inutile de précharger une ligne de cache si celle-ci est déjà dans le cache L1i. L'unité de préchargement doit donc filtrer au mieux les adresses de la FTQ en deux classes : celles qui correspondent à une ligne de cache déjà dans le L1i, celles qui doivent être préchargées. Pour cela, l'unité de préchargement utilise la technique dit du '''''Cache Probe Filtering'''''. L'idée part du principe que le cache d'instruction L1 est multiport. Les ports du cache d'instruction ne sont pas toujours utilisés en même temps et il arrive qu'il y ait un port de lecture de libre. Le CPF utilise alors ce port inutilisé pour vérifier si la prochaine ligne de cache à précharger est dans le cache ou non. Si c'est le cas, on aura un succès de cache : la ligne de cache est oubliée, elle ne sera pas préchargée. Si ce n'est pas le cas on aura un défaut de cache : la ligne sera préchargée. Notez que l'on a pas besoin de lire la ligne en question, juste de vérifier les tags du cache. Dans ce cas, on peut ajouter des signaux de commande spécifiques pour le CPF, qui font une demi-lecture, qui ne vérifie que les tags, mais ne lit pas la donnée. On peut par exemple ajouter un port spécifique pour le CPF, purement en lecture et qui ne permet que de vérifier les tags. Ce port en plus a un cout en circuits plus faible qu'un port de lecture normal, mais ce n'est pas gratuit du tout. ==Le prédécodage d'instructions== La présence d'un cache d'instruction permet l'implémentation de certaines optimisations, dont la plus connue est la technique dite du '''prédécodage'''. Avec elle, lorsque les instructions sont chargées dans le cache d'instruction, elles sont partiellement décodées, grâce à un circuit séparé de l'unité de décodage d'instruction. Le décodage de l'instruction proprement dit est plus court, car une partie du travail est faite en avance, on gagne quelques cycles. [[File:Prédécodage des instructions dans le cache L1.png|centre|vignette|upright=2.5|Prédécodage des instructions dans le cache L1]] Le prédécodage est particulièrement utile avec des instructions de taille variable : il permet de pré-déterminer où commencent/terminent les instructions dans une ligne de cache, indiquer leur taille, etc. Autre possibilité, le prédécodage peut indiquer s'il y a des branchements dans une ligne de cache et où ils se trouvent, ce qui est très utile pour la prédiction de branchement. Pour chaque ligne de cache, le décodage partiel fournit des informations utiles au décodeur d'instruction. Les informations pré-décodées sont soit intégrée dans la ligne de cache, soit mémorisées dans une banque séparée. En clair : une partie de la capacité totale du cache d'instruction est utilisée pour les informations de pré-décodage. Le prédécodage est donc un compromis : un cache d'instruction de plus faible capacité, mais un décodage plus simple. Le pré-décodage est surtout utile pour les instructions qui sont ré-exécutées souvent. Pour les instructions exécutées une seule fois, le gain en performance dépend de l'efficacité du préchargement et d'autres contraintes, mais ce qui est gagné lors du décodage est souvent partiellement perdu lors du prédécodage. Par contre, si une instruction est exécutée plusieurs fois, le pré-décodage est fait une seule fois, alors qu'on a un gain à chaque ré-exécution de l'instruction. ===Les sélecteurs de branchement intégrés au cache L1=== Le pré-décodage peut être utilisé afin de faciliter le travail de la prédiction de branchement. L'idée est d'incorporer une partie de la prédiction de branchement dans le cache L1 d'instruction. Une ligne de cache mémorise alors des informations de prédiction de branchement dans ses bits de contrôle. Les informations en question peuvent être des adresses de destination, ou simplement de quoi déterminer si le branchement est pris ou non. Il s'agit d'une démarche inverse à celle de la ''Fetch Target Queue'', qui découple l'unité de prédiction de branchement du cache, en insérant une mémoire FIFO entre les deux. Là, la démarche est au contraire de fusionner partiellement cache d'instruction et unité de prédiction de branchement. Les premiers processeurs AMD utilisaient cette technique, au moins dans les grandes lignes. Une ligne de cache contient potentiellement plusieurs branchements, dont la position est identifiée par le prédécodage. Pour chaque octet, la ligne de cache associe un bit de contrôle qui indique si un branchement démarre à cet octet, si c'est le premier octet d'un branchement. Le prédécodage peut identifier entre un et plusieurs branchement par ligne de cache, il y a une limite. Le prédécodage n'identifie typiquement que les 3 à 5 premiers branchements, les suivants sont ignorés, faute de place dans les bits de contrôle. Prenons par exemple une ligne de cache de 8 octets, dans laquelle on a 2 branchements de 2 octets chacun. {|class="wikitable" style="text-align:center;" |- ! colspan="16 | Ligne de cache, en octets |- | Instruction || bgcolor="#FFFF00" | Branch 1 || bgcolor="#FFFF00" | Branch 1 || Instruction || bgcolor="#FFFF00" | Branch 2 || bgcolor="#FFFF00" | Branch 2 || Instruction || Instruction |- ! colspan="16 | Bits d'identification des branchements. |- | 0 || 1 || 0 || 0 || 1 || 0 || 0 || 0 |} Il est possible d'améliorer le tout en précisant quel est le type du branchement. Par exemple, on peut distinguer les branchements inconditionnel et conditionnels, ou encore les instruction de retour de fonction. L'intérêt n'est pas évident, mais c'est lié au fait que les branchements inconditionnels sont toujours pris, et que les retour de fonction ont une adresse de destination qui est prédite par une unité de branchement séparée, le ''return adress predictor'', pas par un BTB. Deux bits suffisent pour indiquer : si c'est un branchement conditionnel, inconditionnel, un retour de fonction, ou une instruction qui n'est pas un branchement. {|class="wikitable" style="text-align:center;" |- ! colspan="16 | Ligne de cache, en octets |- | Instruction || bgcolor="#FFFF00" | Saut inconditionnel || bgcolor="#FFFF00" | Saut inconditionnel || Instruction || bgcolor="#A00000" | Branch cond || bgcolor="#A00000" | Branch cond || Instruction || bgcolor="#F0F000" | Retour de fonction |- ! colspan="16 | Bits d'identification des branchements. |- | 00 || 01 || 00 || 00 || 10 || 00 || 00 || 11 |} L'idée est alors d'ajouter, pour chaque branchement détecté, un '''sélecteur de branchement''' qui indique si le branchement est pris ou non. En clair, des informations de prédiction de branchement sont ajoutés à chaque octet de position. Intuitivement, on se dit qu'il y a seulement un bit par branchement, qui indique si le branchement est pris ou non. Les prédictions peuvent venir soit de l'unité de prédiction de branchement, soit provenir du prédécodage. Le prédécodage peut faire de la prédiction statique. Elle peut notamment détecter les branchements inconditionnels et les marquer comme pris. Elle peut aussi détecter les branchements conditionnels et le marquer comme non-pris par défaut. L'unité de prédiction de branchement met à jour les sélecteurs de branchements si besoin, pour les branchements conditionnels. ===L'incorporation du ''Branch Target Buffer'' dans le cache d'instruction=== Une première optimisation permet de se passer de ''Branch Target Buffer''. Pour rappel, celui-ci est un cache qui mémorise, pour chaque branchement, quelle est son adresse de destination. Il peut contenir d'autres informations de prédiction, mais laissons-les de côté pour le moment. L'idée est de déplacer les adresse de destination des branchements dans le cache d'instruction, dans les lignes de cache. Si une ligne de cache contient un branchement, elle mémorise l'adresse de destination de ce branchement, en plus des bits de pré-décodage. En général, les processeurs ne supportent qu'une seule adresse de destination. Si il y a plusieurs branchements dans une ligne de cache, c'est l'adresse de destination du premier branchement pris dans cette ligne de cache qui est mémorisée. Par exemple, l'AMD K5 se passe de ''Branch Target Buffer'' grâce à cela. Il faut cependant remarquer qu'à ce petit jeu, les instructions de retour de fonction sont à part. Leur adresse de destination est souvent donnée par une unité de branchement séparée, le ''return adress predictor'', séparée du ''Branch Target Buffer''. Leurs adresses de destination n'ont pas forcément besoin d'être mémorisées dans les lignes de cache. La technique décrite ici est simple à comprendre. Cependant, les processeurs AMD anciens, d'architecture K6 à K10 n'utilisaient pas cette méthode, mais une variante plus complexe, capable de prédire jusqu'à deux adresses de destination par branchement. A partir de l'architecture K6, le prédécodage déterminait la position des branchements dans les lignes de cache, dans une limite de 4 branchements par ligne de cache. Pour chaque branchement, la ligne de cache mémorisait un sélecteur de branchement, codé sur 2 bits. La valeur des bits indiquait que le branchement n'est pas pris si elle vaut 00, que c'est une instruction de retour de fonction si elle vaut 01, qu'il faut brancher à l'adresse de destination X si elle vaut 10, qu'il faut brancher à l'adresse de destination X si elle vaut 11. Les adresses de destination sont quand à elles mémorisées dans un cache séparé, appelé le ''Branch Target Cache''. Le mécanisme pour adresser ce cache à partir du cache d'instruction n'est pas très détaillé dans la documentation d'AMD. ===Les avantages et inconvénients=== L'avantage de faire ainsi est que la prédiction de branchement est plus rapide. Lire une instruction depuis le cache renvoie non seulement l'instruction lue, mais aussi des informations de prédiction de branchement. L'unité de prédiction de branchement peut alors utiliser ces informations au cycle suivant pour savoir quelle est l'instruction suivante à charger. Un défaut de cette approche est que si le branchement à prédire n'est pas dans le L1 d'instruction, aucune prédiction de branchement ne peut être faite et le préchargement ne peut pas fonctionner. C'est une limitation que n'ont pas les BTB découplées du cache L1 : elles peuvent prédire les adresses de destination et la direction d'un branchement, tant que l'entrée associée est dans le BTB. Et l'entrée peut être conservée, même si l'instruction en question a quitté le cache L1 et qu'elle est dans le L2, le L3 ou même en mémoire RAM. Les prédictions peuvent même servir à précharger les instructions utiles. Sur l'Itanium et l'AMD Opteron, une optimisation assez intéressante permet de conserver les prédictions de branchement lorsque l'un branchement est évincé du cache L1 et se retrouve dans le cache L2. En théorie, les informations de prédiction, présentes dans la ligne de cache, sont perdues lorsque le branchement est évincé. Mais ces processeurs conservent ces prédictions dans un cache séparé, appelé le '''''L2 Branch Cache'''''. ==Le ''Branch Folding'' des CPU PowerPC== Le '''''Branch Folding''''' est une optimisation qui permet d'exécuter les branchements en avance, pendant qu'ils sont encore dans la file d'instruction. Pour cela, une unité spécialisée scanne la file d'instruction pour y trouver les branchements. Quand elle tombe sur un branchement, elle l'exécute en avance et altère le contenu de la file d'instruction pour remplacer les instructions chargées à tord. L'unité en question s'appelle l''''unité de branchements anticipés'''. Pour les branchements inconditionnels, elle peut les "exécuter directement". Elle retire le branchement et le remplace par l'instruction de destination du branchement. Le ''program counter'' est aussi altéré en avance, ce qui fait que les instructions adéquates sont ensuite chargées dans la file d'instruction. Pour les branchements conditionnels, tout dépend de si le processeur implémente la prédiction de branchement ou non. Sans prédiction de branchement, le mieux est de traiter les branchements conditionnels comme non-pris et prier pour que ça fonctionne. Avec prédiction de branchement, tout dépend de si le branchement est prédit comme pris ou non. Si le branchement est considéré comme pris, il est traité comme un branchement inconditionnel : il est remplacé par l'instruction de destination et le ''program counter'' est altéré. SI le branchement est non-pris, il suffit de ne rien faire. Notez qu'ils faut que l'adresse de destination soit connue à l'avance, ou du moins puisse être calculée facilement (branchements relatifs). Les premiers processeurs PowerPC utilisaient cette optimisation. Ils avaient une file d'instruction de 8 instructions, et l'unité de branchement scannait les 4 premières instructions. L'unité de branchements anticipés contenait les registres nécessaires pour gérer les branchements. Elle contenait précisément le ''link register'' pour des appels de procédures, le ''Count Target Register'' et le ''Count Register'' utilisés pour les boucles. Il contient aussi un additionneur pour calculer les adresses des branchements relatifs. L'unité de branchements anticipés doit identifier les branchements, ce qui demande de décoder partiellement les instructions en attente. Une solution pratique est d'utiliser la technique du pré-décodage d'instruction, de la section précédente. Le pré-décodage est parfait pour identifier les branchements dans un bloc d'instruction. Les informations prédécodées peuvent alors être utilisées pour remplir la file d'instruction. Chaque instruction dans la file d'instruction est associée à un bit qui indique si c'est un branchement ou non. L'unité de branchement anticipé peut alors détecter les branchements avec un simple encodeur à priorité. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=La prédiction de branchement | prevText=La prédiction de branchement | next=Les pipelines multicycles | nextText=Les pipelines multicycles }} </noinclude> {{AutoCat}} 0x4ys06gdiybcf3zg5jwnnczpssalcm Mathc initiation/a522 0 80975 772812 772166 2026-09-22T11:45:41Z Xhungab 23827 772812 wikitext text/x-wiki __NOTOC__ [[Catégorie:Mathc initiation (livre)]] [[Mathc initiation/Sommaire#Analyse IV|Sommaire]] Je vous propose comme cours de référence les trois livres de '''openstax''' en accès libre. Vous pouvez les lire en ligne ou télécharger les PDF. * [https://openstax.org/details/books/calculus-volume-1 Calculus 1], * [https://openstax.org/details/books/calculus-volume-2 Calculus 2], * [https://openstax.org/details/books/calculus-volume-3 Calculus 3]. : . : {{Partie{{{type|}}}|[[Mathc initiation/a10| Analyse IV : Les équations différentielles]]}} . {{Partie{{{type|}}}|[[Mathc initiation/a594| Analyse IV : Se familiariser avec les séries de Fourier]]}} . {{Partie{{{type|}}}|[[Mathc initiation/a592| Analyse IV : Se familiariser avec la transformée de Fourier discrète]]}} . {{Partie{{{type|}}}|[[Mathc initiation/a512| Analyse IV : Se familiariser avec la Transformée de Laplace]]}} {{Partie{{{type|}}}|[[Mathc initiation/006y| Analyse IV : la Transformée Inverse de Laplace]]}} {{Partie{{{type|}}}|[[Mathc initiation/006s| Analyse IV : la Transformée de Laplace : Quelques exercices]]}} {{Partie{{{type|}}}|[[Mathc initiation/006r| Analyse IV : la Transformée de Laplace : Quelques applications]]}} . {{Partie{{{type|}}}|[[Mathc initiation/a584| Analyse IV : Se familiariser avec la transformée en Z]]}} {{Partie{{{type|}}}|[[Mathc initiation/0073| Analyse IV : La transformée en Z : Quelques astuces]]}} {{AutoCat}} awr50zft2mq3vlk1mqnf9kz2kzrm5xu Mathc initiation/a582 0 81064 772665 772657 2026-09-21T13:10:15Z Xhungab 23827 772665 wikitext text/x-wiki __NOTOC__ [[Catégorie:Mathc initiation (livre)]] [[Mathc initiation/a584#* Les suites usuelles :| Sommaire]] {{Partie{{{type|}}}|Transformées en Z usuelles, des signaux causales discrets :}} Pour simplifier l'écriture j'ai écrit le signal f(n) au lieu du '''signal causale discret f(n) u(n)'''. '''Le signal : ''' '''Transformée en Z : ''' Domaine de convergence '''f(n)''' '''F(z)''' '''δ(n) = 1''' '''1''' C '''* L'impulsion unitaire''' '''u(n) = 1''' '''[[Mathc initiation/0070#L'échelon unitaire : u(n) = 1|z/(z-1)]]''' |z| > 1 '''* L'échelon unitaire''' '''r(n) = n''' '''[[Mathc initiation/0070#La rampe : r(n) = n|z/(z-1)^2]]''' |z| > 1 '''* La rampe''' '''c(n) = n^2''' '''[[Mathc initiation/0070#Le carré : c(n) = n^2|z(z+1)/(z-1)^3]]''' |z| > 1 '''* Le carré''' '''f(n) = a^n''' '''[[Mathc initiation/0070#L'exponentiel : f(n) = a^n|z/(z-a)]]''' |z| >|a| '''* L'exponentiel''' '''g(n) = cos(kn)''' '''[[Mathc initiation/0071#La fonction cosinus discret : g(n) = cos(kn)|[z^2-z cos(k)]/[z^2-2z cos(k)+1] ]]''' |z| > 1 '''* La fonction cosinus discret''' '''h(n) = sin(kn)''' '''[[Mathc initiation/0072#La fonction sinus discret : h(n) = sin(kn)|[z sin(k)]/[z^2-2z cos(k)+1] ]]''' |z| > 1 '''* La fonction sinus discret''' '''Un signal discret causal est une suite de valeurs x[n] qui est entièrement nulle pour tous les instants de temps négatifs (n < 0)''' {{AutoCat}} 4w0jri34asq2gmdu72n360lkuurc2a4 772698 772665 2026-09-21T17:11:12Z Xhungab 23827 772698 wikitext text/x-wiki __NOTOC__ [[Catégorie:Mathc initiation (livre)]] [[Mathc initiation/a584#* Les suites usuelles :| Sommaire]] {{Partie{{{type|}}}|Transformées en Z usuelles, des signaux causales discrets :}} Pour simplifier l'écriture j'ai écrit le signal f(n) au lieu du '''signal causale discret f(n) u(n)'''. '''Le signal : ''' '''Transformée en Z : ''' Domaine de convergence '''x(n)''' '''X(z)''' '''δ(n) = 1''' '''1''' C '''* L'impulsion unitaire''' '''u(n) = 1''' '''[[Mathc initiation/0070#L'échelon unitaire : u(n) = 1|z/(z-1)]]''' |z| > 1 '''* L'échelon unitaire''' '''r(n) = n''' '''[[Mathc initiation/0070#La rampe : r(n) = n|z/(z-1)^2]]''' |z| > 1 '''* La rampe''' '''c(n) = n^2''' '''[[Mathc initiation/0070#Le carré : c(n) = n^2|z(z+1)/(z-1)^3]]''' |z| > 1 '''* Le carré''' '''f(n) = a^n''' '''[[Mathc initiation/0070#L'exponentiel : f(n) = a^n|z/(z-a)]]''' |z| >|a| '''* L'exponentiel''' '''g(n) = cos(kn)''' '''[[Mathc initiation/0071#La fonction cosinus discret : g(n) = cos(kn)|[z^2-z cos(k)]/[z^2-2z cos(k)+1] ]]''' |z| > 1 '''* La fonction cosinus discret''' '''h(n) = sin(kn)''' '''[[Mathc initiation/0072#La fonction sinus discret : h(n) = sin(kn)|[z sin(k)]/[z^2-2z cos(k)+1] ]]''' |z| > 1 '''* La fonction sinus discret''' '''Un signal discret causal est une suite de nombres x(n) définie pour des indices entiers n qui s'annule pour tous les temps négatifs (n < 0).''' {{AutoCat}} cj3ia67vegdjjk5jr9qbvz5ihlxjexo Mathc initiation/a583 0 81065 772701 772489 2026-09-21T17:23:30Z Xhungab 23827 772701 wikitext text/x-wiki __NOTOC__ [[Catégorie:Mathc initiation (livre)]] [[Mathc initiation/a584#* Les suites usuelles :| Sommaire]] {{Partie{{{type|}}}|Propriétés des transformées en Z :}} Soit x(n) et y(n) deux signaux causaux discrets : '''* La transformée en Z est un opérateur linéaire :''' '''Z[a x(n) + b y(n)] = a Z[x(n)] + b Z[y(n)] = a X(z) + b Y(z)''' '''* Multiplication par une exponentielle : [[Mathc initiation/a585|Exemples]] ''' a^n x(n) Si Z[x(n)] = X(z) alors '''Z[a^n x(n)] = X(z/a)''' '''* Multiplication par la variable d'évolution : [[Mathc initiation/a588|Exemples]] ''' n x(n) Si Z[x(n)] = X(z) alors '''Z[n x(n)] = -z X'(z)''' '''* Le retard de no unités : [[Mathc initiation/a589|Exemples]] ''' x(n-no) est un signal causal retardé de no unités. Image (L'invité retardé de no minutes, arrivera à 12h+no minutes) Si Z[x(n)] = X(z) alors '''Z[x(n-no)] = z^(-no) X(z)''' '''* L'avance d'une unité : [[Mathc initiation/a590|Exemples]] ''' x(n+1) est un signal causal avancé de 1 unité. Image (L'invité en avance de 1 minute, arrivera à 12h-1 minute, 11h59 minutes) Si Z[x(n)] = X(z) alors '''Z[x(n+1)] = z^(1) [X(z)-x(0)]''' '''* L'avance de deux unités : [[Mathc initiation/a591|Exemples]]''' x(n+2) est un signal causal avancé de 2 unité. Image (L'invité en avance de 2 minutes, arrivera à 12h-2 minutes, 11h58 minutes) Si Z[x(n)] = X(z) alors '''Z[x(n+2)] = z^(2) [X(z)-x(0)-x(1)z^(-1)]''' : {{AutoCat}} mm30k2156gwgbcucn3rb5d8d3ij22t6 772705 772701 2026-09-21T17:37:05Z Xhungab 23827 772705 wikitext text/x-wiki __NOTOC__ [[Catégorie:Mathc initiation (livre)]] [[Mathc initiation/a584#* Les suites usuelles :| Sommaire]] {{Partie{{{type|}}}|Propriétés des transformées en Z :}} Soit x(n) et y(n) deux signaux causaux discrets : '''* La transformée en Z est un opérateur linéaire :''' '''Z[a x(n) + b y(n)] = a Z[x(n)] + b Z[y(n)] = a X(z) + b Y(z)''' '''* Multiplication par une exponentielle : [[Mathc initiation/a585|Exemples]] ''' a^n x(n) Si Z[x(n)] = X(z) alors '''Z[a^n x(n)] = X(z/a)''' '''* Multiplication par la variable d'évolution : [[Mathc initiation/a588|Exemples]] ''' n x(n) Si Z[x(n)] = X(z) alors '''Z[n x(n)] = -z X'(z)''' '''* Le retard de no unités : [[Mathc initiation/a589|Exemples]] ''' x(n-no) est un signal causal retardé de no unités. Image (L'invité retardé de no minutes, arrivera à 12h+no minutes) Si Z[x(n)] = X(z) alors '''Z[x(n-no)] = z^(-no) X(z)''' '''* L'avance d'une unité : [[Mathc initiation/a590|Exemples]] ''' x(n+1) est un signal causal avancé de 1 unité. Image (L'invité en avance de 1 minute, arrivera à 12h-1 minute, 11h59 minutes) Si Z[x(n)] = X(z) alors '''Z[x(n+1)] = z^(1) [X(z)-x(0)]''' '''* L'avance de deux unités : [[Mathc initiation/a591|Exemples]]''' x(n+2) est un signal causal avancé de 2 unité. Image (L'invité en avance de 2 minutes, arrivera à 12h-2 minutes, 11h58 minutes) Si Z[x(n)] = X(z) alors '''Z[x(n+2)] = z^(2) [X(z)-x(0)-x(1)z^(-1)]''' Remarque : Lors d'une avance, certains éléments de la suite x(n) peuvent passer du côté négatif. Dans ce cas, il suffit d'ignorer ces éléments : -1 0 1 2 3 4 x(0) x(1) x(2) x(3) x(4) <- x(0) x(1) x(2) x(3) x(4) x(5) L'avance d'une unité. x(1) x(2) x(3) x(4) x(5) {{AutoCat}} hmw3pld5ujgkh8y5mfu76m58j6h4fas 772811 772705 2026-09-22T11:21:48Z Xhungab 23827 772811 wikitext text/x-wiki __NOTOC__ [[Catégorie:Mathc initiation (livre)]] [[Mathc initiation/a584#* Les suites usuelles :| Sommaire]] {{Partie{{{type|}}}|Propriétés des transformées en Z :}} Soit x(n) et y(n) deux signaux causaux discrets : '''* La transformée en Z est un opérateur linéaire :''' '''Z[a x(n) + b y(n)] = a Z[x(n)] + b Z[y(n)] = a X(z) + b Y(z)''' '''* Multiplication par une exponentielle : [[Mathc initiation/a585|Exemples]] ''' a^n x(n) Si Z[x(n)] = X(z) alors '''Z[a^n x(n)] = X(z/a)''' '''* Multiplication par la variable d'évolution : [[Mathc initiation/a588|Exemples]] ''' n x(n) Si Z[x(n)] = X(z) alors '''Z[n x(n)] = -z X'(z)''' '''* Le retard de no unités : [[Mathc initiation/a589|Exemples]] ''' x(n-no) est un signal causal retardé de no unités. Image (L'invité retardé de no minutes, arrivera à 12h+no minutes) Si Z[x(n)] = X(z) alors '''Z[x(n-no)] = z^(-no) X(z)''' '''* L'avance d'une unité : [[Mathc initiation/a590|Exemples]] ''' x(n+1) est un signal causal avancé de 1 unité. Image (L'invité en avance de 1 minute, arrivera à 12h-1 minute, 11h59 minutes) Si Z[x(n)] = X(z) alors '''Z[x(n+1)] = z^(1) [X(z)-x(0)]''' '''* L'avance de deux unités : [[Mathc initiation/a591|Exemples]]''' x(n+2) est un signal causal avancé de 2 unité. Image (L'invité en avance de 2 minutes, arrivera à 12h-2 minutes, 11h58 minutes) Si Z[x(n)] = X(z) alors '''Z[x(n+2)] = z^(2) [X(z)-x(0)-x(1)z^(-1)]''' Remarque : Lors d'une avance, certains éléments de la suite x(n) peuvent passer du côté négatif. Dans ce cas, il suffit d'ignorer ces éléments : -1 0 1 2 3 4 x(0) x(1) x(2) x(3) x(4) <- x(0) x(1) x(2) x(3) x(4) x(5) L'avance d'une unité. x(1) x(2) x(3) x(4) x(5) {{AutoCat}} osjeokcxnvcrodbcvrgolfjo9b0avb2 Mathc initiation/a585 0 81067 772706 772385 2026-09-21T17:41:39Z Xhungab 23827 772706 wikitext text/x-wiki __NOTOC__ [[Catégorie:Mathc initiation (livre)]] [[Mathc initiation/a583| Sommaire]] {{Partie{{{type|}}}|Multiplication par une exponentielle : a^n x(n) u(n)|fond={{{fond|}}<nowiki>}</nowiki>}} Pour simplifier l'écriture j'ai écrit le signal x(n) au lieu du '''signal causale discret x(n) u(n)'''. '''Le signal''' '''Transformée en Z''' '''x(n)''' '''X(z)''' '''Z[a^n x(n)] = X(z/a)''' '''u(n)''' '''z/(z-1)''' '''(z/a)/((z/a)-1)''' = '''z/(z-a)''' '''n''' '''z/(z-1)^2''' '''(z/a)/((z/a)-1)^2''' = '''az/(z-a)^2''' '''n^2''' '''z(z+1)/(z-1)^3''' '''(z/a)((z/a)+1)/((z/a)-1)^3''' = '''(a+z)az/(z-a)^3''' '''a^n''' '''z/(z-a)''' '''(z/a)/((z/a)-a)''' = '''z/(z-a^2)''' Le signal g(n) : '''cos(kn)''' Transformée en Z G(z) : '''[z^2-z cos(k)]/[z^2-2z cos(k)+1]''' Z[a^n g(n)] : '''[(z/a)^2-(z/a)cos(k)]/[(z/a)^2-2(z/a)cos(k)+1]''' G(z/a) : '''[z^2-az cos(k)]/[z^2-2az cos(k)+a^2]''' Le signal h(n) : '''sin(kn)''' Transformée en Z H(z) : '''[z sin(k)]/[z^2-2z cos(k)+1]''' Z[a^n h(n)] : '''[(z/a) sin(k)]/[(z/a)^2-2(z/a)cos(k)+1]''' H(z/a) : '''[az sin(k)]/[z^2-2az cos(k)+a^2]''' '''Un signal discret causal est une suite de valeurs x[n] qui est entièrement nulle pour tous les instants de temps négatifs (n < 0)''' {{AutoCat}} i5te8y2id9b15qfxb7app1q8vo3yay6 Mathc initiation/a589 0 81072 772707 772381 2026-09-21T17:44:49Z Xhungab 23827 772707 wikitext text/x-wiki __NOTOC__ [[Catégorie:Mathc initiation (livre)]] : : [[Mathc initiation/a583| Sommaire]] : ----{{Partie{{{type|}}}|Le retard de no unités : x(n-no)|fond={{{fond|}}<nowiki>}</nowiki>}} Pour simplifier l'écriture j'ai écrit le signal x(n) au lieu du '''signal causale discret x(n) u(n)'''. '''Le signal''' '''Transformée en Z ''' '''x(n)''' '''X(z)''' '''Z[x(n-no)] = z^(-no) X(z)''' '''u(n)''' '''z/(z-1)''' '''z^(-no) z/(z-1)''' '''n''' '''z/(z-1)^2''' '''z^(-no) z/(z-1)^2''' '''n^2''' '''z(z+1)/(z-1)^3''' '''z^(-no) z(z+1)/(z-1)^3''' '''a^n''' '''z/(z-a)''' '''z^(-no) z/(z-a)''' '''cos(kn)''' '''[z^2-z cos(k)]/[z^2-2z cos(k)+1]''' '''z^(-no) [z^2-z cos(k)]/[z^2-2z cos(k)+1]''' '''sin(kn)''' '''[z sin(k)]/[z^2-2z cos(k)+1]''' '''z^(-no) [z sin(k)]/[z^2-2z cos(k)+1]''' '''Un signal discret causal est une suite de valeurs x[n] qui est entièrement nulle pour tous les instants de temps négatifs (n < 0)''' {{AutoCat}} tbyfejollo8tr5kt57ygyd4b8ixbx72 Mathc initiation/a590 0 81073 772708 772383 2026-09-21T17:48:36Z Xhungab 23827 772708 wikitext text/x-wiki __NOTOC__ [[Catégorie:Mathc initiation (livre)]] : : [[Mathc initiation/a583| Sommaire]] : ----{{Partie{{{type|}}}|L'avance d'une unité : x(n+1)|fond={{{fond|}}<nowiki>}</nowiki>}} Pour simplifier l'écriture j'ai écrit le signal x(n) au lieu du '''signal causale discret x(n) u(n)'''. '''Le signal ''' '''Transformée en Z''' '''Z[f(n+1)]''' '''x(n)''' '''X(z)''' '''z^(1) [ X(z) - x(0)]''' '''u(n)''' '''z/(z-1)''' '''z^(1) [ [z/(z-1)] - u(0)]''' '''n''' '''z/(z-1)^2''' '''z^(1) [ [z/(z-1)^2] - 0]''' '''n^2''' '''z(z+1)/(z-1)^3''' '''z^(1) [ [z(z+1)/(z-1)^3] - 0^2]''' '''a^n''' '''z/(z-a)''' '''z^(1) [ [z/(z-a)] - a^0]''' '''cos(kn)''' '''[z^2-z cos(k)]/[z^2-2z cos(k)+1]''' '''z^(1) [z^2-z cos(k)]/[z^2-2z cos(k)+1] - cos(k0)]''' '''sin(kn)''' '''[z sin(k)]/[z^2-2z cos(k)+1]''' '''z^(1) [ [z sin(k)]/[z^2-2z cos(k)+1] - sin(k0)]''' : ---- {{AutoCat}} nj0fvekvwq6houxi6ibk174eb97nhbc Mathc initiation/a591 0 81074 772709 772384 2026-09-21T17:53:24Z Xhungab 23827 772709 wikitext text/x-wiki __NOTOC__ [[Catégorie:Mathc initiation (livre)]] [[Mathc initiation/a583| Sommaire]] {{Partie{{{type|}}}|L'avance de deux unités : x(n+2)|fond={{{fond|}}<nowiki>}</nowiki>}} Pour simplifier l'écriture j'ai écrit le signal x(n) au lieu du '''signal causale discret x(n) u(n)'''. '''Le signal''' '''Transformée en Z ''' '''Z[x(n+2)]''' '''x(n)''' '''X(z)''' '''z^2 [ X(z) -x(0) -x(1) z^(-1)]''' '''u(n)''' '''z/(z-1)''' '''z^2 [ z/(z-1) -u(0) -u(1) z^(-1)]''' '''n''' '''z/(z-1)^2''' '''z^2 [ z/(z-1)^2 -0 -1 z^(-1)]''' '''n^2''' '''z(z+1)/(z-1)^3''' '''z^2 [z(z+1)/(z-1)^3 -0^2 -1^2 z^(-1)]''' '''a^n''' '''z/(z-a)''' '''z^2 [ z/(z-a) -a^0 -a^1 z^(-1)]''' '''Le signal g(n)''' : '''cos(kn)''' '''Transformée en Z ''' : '''[z^2-z cos(k)]/[z^2-2z cos(k)+1]''' '''Z[g(n+2)]''' : '''z^2 [[z^2-z cos(k)]/[z^2-2z cos(k)+1] -cos(k0) -cos(k1) z^(-1)]''' '''Le signal h(n)''' : '''sin(kn)''' '''Transformée en Z ''' : '''[z sin(k)]/[z^2-2z cos(k)+1]''' '''Z[h(n+2)]''' : '''z^2 [[z sin(k)]/[z^2-2z cos(k)+1] -sin(k0) -sin(k1) z^(-1)]''' {{AutoCat}} py6jxifln01xykbh73zkid8jx87jrh3 Fonctionnement d'un ordinateur/Exemples de microarchitectures CPU : le cas du x86 0 82608 772750 772452 2026-09-21T21:01:00Z Mewtow 31375 /* La microarchitecture Core */ 772750 wikitext text/x-wiki Dans ce chapitre, nous allons étudier des exemples de processeurs x86, ceux présents dans nos PC. Nous n'allons pas voir les anciens processeurs comme le 286, le 386 ou le 486. Nous allons commencer avec le Pentium 1, et les processeurs commerciaux qui ont suivis. Tous les processeurs que nous allons voir dans ce chapitre sont des processeurs superscalaires. De fait, ce n'est pas pour rien si ce chapitre se situe après le chapitre sur les processeurs superscalaires. Par contre, nous allons voir que certains n'ont pas d'exécution dans le désordre. La raison est que l'exécution dans le désordre est arrivé après la superscalarité. ==Généralités sur les CPU x86 superscalaires== Avant de voir chaque processeur indépendamment des autres, nous allons devoir aborder quelques généralités. Nous allons d'abord voir que le jeu d'instruction x86 pose quelques problèmes pour la superscalarité. Et ces problèmes posent des contraintes assez fortes, avec lesquelles les concepteurs de processeurs dovient faire avec. Nous poursuivrons ensuite par un historique des processeurs Intel et AMD, histoire de donner un peu de contexte aux processeurs que nous allons étudier. ===Le jeu d'instruction x86 pose des problèmes pour la superscalarité=== Une difficulté de l'architecture x86 est qu'il s'agit d'une architecture CISC, avec tous les défauts que ça implique. Un jeu d'instruction CISC a en effet de nombreuses propriétés qui collent mal avec l'émission multiple, avec la '''superscalarité'''. Il y en a plusieurs, certaines impactent le chargement des instructions, d'autres leur décodage, d'autres l'exécution, etc. Premièrement, les instructions sont de longueur variable, entre 1 et 15 octets, ce qui complique leur chargement et leur décodage. En pratique, les processeurs chargent un bloc de 32 à 64 octets, et découpent celui-ci en plusieurs instructions. La conséquence est que l'usage d'instructions trop longues peut poser problème. Imaginez qu'un processeur charge un bloc de 16 octets et que celui-ci ne contienne qu'une seule instruction : on ne profite pas de la superscalarité. Deuxièmement, une partie des instructions est microcodée, faute de mieux. Et cela pose de sérieux challenges pour l'implémentation des décodeurs. Dupliquer le microcode demanderait trop de transistors, ce qui fait que ce n'est pas fait. À la place, il n'y a qu'un seul microcode, ce qui fait que l'on ne peut pas décoder plusieurs instructions microcodées en même temps. Il est cependant possible de profiter de la superscalarité, en décodant une instruction microcodée en parallèle d'autres instructions non-microcodées. Et heureusement, ce cas est de loin le plus fréquent, il est rare que plusieurs instructions microcodées se suivent. Troisièmement, la présence d'instructions ''load-up'', qui lisent un opérande en mémoire, peut poser problème, mais est aussi source d'optimisations assez intéressantes. En théorie, une instruction ''load-op'' est décodée en deux micro-opération : une pour lire d'opérande en RAM, l'autre pour faire l'opération arithmétique. Sauf que les processeurs x86 modernes optimisent la gestion des instructions ''load-up''. Par exemple, les premiers processeurs Atom géraient des micro-opérations de type ''load-up'', directement dans le chemin de données ! D'autres processeurs utilisent la technique de la '''micro-fusion''' pour retarder le décodage réel des instructions ''load-up'' assez loin dans le pipeline. Avec eux, une instruction ''load-op'' est décodée en une seule "macro-opération", qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. L'avantage est qu'une macro-opération ne prend qu'une seule entrée dans le tampon de ré-ordonnancement, la fenêtre d'instruction, la file de micro-opération, et les autres structures similaires. ===Un petit historique des processeurs x86 superscalaires=== Nous allons voir les processeurs Intel à part des processeurs AMD. La raison à cela est que les architectures Intel et AMD ont progressivement évolué, chacune se basant sur la précédente et l'améliorant. Il n'y a pas eu de cassure entre microarchitectures AMD, qui sont chacune la suite de la précédente. Il est donc préférable de voir les architectures AMD dans l'ordre chronologique. Par contre, Intel a eu une gigantesque cassure, avec le processeur Pentium 4. Son architecture se démarquait fortement du Pentium 3, mais elle n'a pas convaincu et a été abandonnée avec les processeurs suivants. Ce qui fait nous verrons l'architecture du Pentium 4 à part. Le Pentium 1 et 2 utilisaient la même architecture, qu'on détaillera dans ce qui suit. Les seules différences importantes étaient la fréquence, le cache, et quelques détails dans le genre. Le Pentium 3 était une nouvelle microarchitecture qui ajoutait l'exécution dans le désordre. Un an et demi plus tard, le Pentium 4 est sorti et a été un échec. Ses performances étaient peu convaincantes face au Pentium 3, et sa consommation énergétique était très importante. La conséquence est que le Pentium 4 et le Pentium 3 ont survécu pendant un long moment, beaucoup de monde préférait acheter un Pentium 3. Intel a alors amélioré les microarchitectures du Pentium 3 et du 4, indépendamment, pendant environ 7 ans. La microarchitecture du Pentium 3 a subit plusieurs micro-évolutions, chacune avec une finesse de gravure différente, afin de satisfaire les consomateurs. Les premeirs Pentium 3 avaient une finesse de gravure de 250 nm, elle a chuté à 65 sur les derniers modèles. L'architecture du Pentium 4 a fait la même chose, pour tenter de corriger ses problèmes de performance et de consommation d'énergie. Les premiers Pentium 4 avaient uen finesse de gravure de 180 nm, elle a elle aussi chutée à 65 sur les derniers modèles. Après l'échec du Pentium 4, les ingénieurs d'Intel ont repris l'architecture P6 et l'ont améliorée fortement, pour donner l'architecture Core. Les micro-processeurs suivants ont fait évoluer cette architecture progressivement, au point où elle ne ressemble plus à l'originale. L'architecture Core a laissé la place à l'architecture Nehalem, puis Sandy Bridge, puis Haswell, puis Skylake, puis Ice Lake, et Golden Cove. Il s'agit de la lignée principale, partant du Pentium 3 et continuant jusqu'à nos jours. Ces microarchitectures ont suivi un motif assez simple, appelé modèle '''tick-tock'''. Chaque microarchitecture était déclinée en deux versions, la seconde ayant une finesse de gravure réduite. En parallèle, Intel a travaillé sur des processeurs basse performance et basse consommation, avec une microarchitecture très différente. Les processeurs Atom de microarchitecture Bonnel, pour être ensuite remplacés par les microarchitectures Silvermont, puis Goldmont et Gracemont. Ces microarchitectures ont évolué en parallèle de la lignée principale, il s'agit d'une lignée secondaire. Le tout est résumé dans ce schéma ci-dessous. [[File:IntelProcessorRoadmap-4v.svg|centre|vignette|upright=2.5|Roadmap des processeurs Intel, qui servira de structure pour la suite du chapitre]] De nos jours, les processeurs Intel utilisent les deux types de micro-architectures en même temps. Les CPU Intel modernes disposent de deux types de coeurs : les coeurs P et le coeurs E. Leur nom signifie "Performance" et "Efficient", qui trahissent leur but. Les coeurs P utilisent la microarchitecture de la lignée principale, qui est conçue pour la performance. Les coeurs E, quant à eux, utilisent les micro-architectures basse consommation de la lignée secondaire. ==Les processeurs x86 d'Intel, la lignée principale== Pour commencer, nous allons voir les processeurs Intel. N'y voyez pas du favoritisme derrière ce choix, la justification est toute autre. Si je commence par Intel, c'est pour commencer avec les Pentium 1 et 2, qui sont plus simples que tous les autres. Il s'agit en effet de processeurs superscalaires, mais sans exécution dans le désordre. L’absence d'exécution dans le désordre les rend bien plus simples à étudier que les autres, ce qui en fait un bon point de départ pour ce chapitre. Le successeur du Pentium 2 a intégré l'exécution dans le désordre, ce qui fait que les Pentium 1 et 2 sont les seuls processeurs superscalaires ''in-order''. Pour la concurrence, AMD n'a pas produit de processeurs ''in-order'', tous les processeurs produits par AMD intègrent l'exécution dans le désordre. A la rigueur, il y a bien les processeurs Atom de la lignée secondaire d'Intel. Cependant, même s'ils sont bien des CPU ''in-order'', leur architecture est assez compliquée. En comparaison, le Pentium est une vieille architecture, qui se débrouillait avec peu de transistors et était donc bien plus simple que celle de l'Atom. ===Le Pentium 1/MMX et les pipelines U/V=== Le processeur Pentium d'Intel avait un pipeline de 5 étages : un étage de chargement/prédiction de branchement, deux étages de décodage, un étage d'exécution et un dernier étage pour l'écriture dans les registres. Le Pentium 1 était un processeur double émission, intégrant deux pipelines nommés U et V. Chose importante, les deux pipelines n'étaient pas identiques. Le pipeline U pouvait exécuter toutes les instructions, mais le pipeline V était beaucoup plus limité. Par exemple, seul le pipeline U peut faire des calculs flottants, le pipeline V ne fait que des calculs entiers et des branchements. Les deux pipelines disposaient d'une unité de calcul entière, identique dans les deux pipelines. Mais le pipeline U incorporait un circuit multiplieur/diviseur et d'un ''barrel shifter''. L'unité flottante était sur le port d'émission du pipeline U, idem pour l'unité de calcul vectoriel MMX sur le Pentium MMX. Les deux pipelines avaient chacun une unité de calcul d'adresse, mais ils n'étaient pas identiques : celle du pipeline V ne gérait que l’instruction LEA, celle du pipeline U gérait tous les calculs d'adresse. {|class="wikitable" |- ! Pipeline U ! Pipeline V |- | ALU entière | ALU entière |- | Multiplieur/diviseur | |- | ''Barrel Shifter'' | |- | AGU complexe | AGU simple (opération LEA) |- | FPU | |- | Unité SIMD | |} Les deux pipelines géraient les opérations bit à bit, les additions, les soustractions et les comparaisons. Les autres instructions ne sont exécutables que dans le pipeline U. Pour être plus précis, les deux pipelines supportaient les instructions suivantes, ce qui fait qu'on pouvait en faire deux en même temps : * Les instructions arithmétiques INC, DEC, ADD, SUB ; * l'instruction de comparaison CMP ; * les instructions bit à bit AND, OR, XOR ; * l'instruction de calcul d'adresse LEA ; * l'instruction MOV (dépend du mode d'adressage) ; * les instructions de gestion de la pile PUSH et POP (dépend du mode d'adressage) ; * l'instruction NOP, qui ne fait rien. Il faut noter qu'il y a cependant quelques restrictions, beaucoup de paires d'instructions sont interdites. La plupart interdisent au pipeline V de faire quoique ce soit quand une opération particulière est émise dans le pipeline U. Par exemple, si le pipeline U exécute une multiplication ou une division, le processeur ne peut pas exécuter une opération dans le pipeline V. Et c'est pareil avec les branchements : si un branchement est émis dans le pipeline U, l'instruction suivant le branchement n'est pas émise dans le pipeline V, pour éliminer les dépendances de contrôle. De même, si le pipeline U exécute une opération flottante, le pipeline V ne pourra rien exécuter. La seule exception est l'instruction FCXH, qui échange deux registres flottants. [[File:Intel Pentium arch.svg|centre|vignette|upright=2.5|Microarchitecture de l'Intel Pentium MMX. On voit que certaines unités de calcul sont dupliquées.]] Un choix assez intéressant a été fait pour le cache de données. Nous avions vu dans le chapitre sur les CPU superscalaires que la superscalarité a un impact sur l'unité mémoire. Il y a alors deux implémentations. La première ne fait rien, l'unité mémoire ne change pas, et le processeur ne peut pas faire deux accès mémoire simultanés. La seconde duplique l'unité mémoire et les ports de lecture/écriture du cache, ce qui autorise des accès mémoire simultanés. Le Pentium 1 utilise une solution intermédiaire. Les ingénieurs d'Intel étaient partis à la base sur un cache totalement double port, pour obtenir des performances maximales. Mais diverses simulations et observations les ont fait changer d'avis. Les simulations ont montré qu'il est "rare" que les deux pipelines aient besoin de lire/écrire dans le cache en même temps. Et ils ont optimisé le cache de donnée pour en tenir compte. Le cache de données est partiellement multiport : simple port sur certains aspects, double port sur d'autres. Le cache est un cache splité, à savoir que les données et les ''tags'' sont séparés dans des mémoires séparées. La mémoire pour les ''tags'' est multiport, ce qui permet d'interroger les ''tags'' du cache deux fois par cycle. Un port est relié au pipeline u, un autre au pipeline V. Mais pour les données, le cache n'a qu'un seul port pour lire/écrire des données. Impossible donc de lire deux données en même temps, pour alimenter les deux pipelines. Il utilise cependant 8 banques permet d'accélérer des accès mémoire proches dans le temps, mais dans des cycles d'horloge différents. Entre les deux mémoires, il y a un circuit qui détecte les conflits, à savoir les situations où les deux pipelines accèdent en même temps au cache. S'ils veulent lire/écrire une donnée dans le même cycle, ce qui est impossible avec un seul port, le pipeline U a la priorité et le pipeline V attend le cycle suivant. Le circuit détecte aussi les conflits de banque, à savoir quand un pipeline accède à une banque en cours d'accès par l'autre pipeline (on rappelle qu'un accès au cache prend plusieurs cycles). Le circuit détecte aussi certaines dépendances mémoires, à savoir des accès consécutifs à la même adresse. : La TLB du processeur est aussi totalement double port. ===La microarchitecture P6 du Pentium 2/3=== Le Pentium 3 utilisait la '''microarchitecture P6''', qui a été dérivée dans de nombreuses variantes, dont les finesses de gravure n'étaient pas les mêmes. Il introduit une exécution dans le désordre simple, avec une fenêtre d'instruction centralisée, avec renommage dans le désordre dans le ROB (tampon de ré-ordonnancement), commandé par une table d'alias. C'était un processeur triple émission, soit une instruction de plus que la double émission du Pentium 1. Le pipeline passe de 5 étage sur le Pentium à 14 - 12 étages, dont le détail est le suivant : * Prédiction de branchement, deux cycles ; * Chargement des instructions, trois cycles ; * Décodage de l'instruction, deux cycles ; * Renommage de registre, un cycle ; * Copie des opérandes dans le tampon de ré-ordonnancement (lié au renommage de registre dans le ROB) ; * Dispath dans ou depuis la station de réservation. * Exécution de l'instruction ; * Écriture du résultat dans le ROB ; * Écriture dans le banc de registre physique. Les instructions sont chargées par blocs de 16 octets, avec un système de fusion de blocs pour gérer les instructions à cheval sur deux blocs. Lors d'un branchement, deux blocs doivent être chargés si l'instruction de destination n'est pas alignée sur 16 octets et cela cause un délai de un cycle d'horloge. Le décodage des instructions x86 était géré par plusieurs décodeurs. Il y avait trois décodeurs : deux décodeurs simples, et un décodeur complexe. Les décodeurs simples décodaient les instructions les plus fréquentes, mais aussi les plus simples, qui étaient décodées en une seule micro-opération. Les instructions CISC complexes étaient gérées uniquement par le décodeur complexe, basé sur un microcode, qui pouvait fournir jusqu'à 4 micro-opérations par cycle. Le tout est résumé avec la règle 4-1-1. La toute première instruction chargée depuis la file d'instruction va dans le premier décodeur simple. Si jamais le décodeur ne peut pas décoder l'instruction, l'instruction est redirigée dans un autre décodeur, avec un délai d'un cycle d'horloge. Les stations de réservations étaient regroupées dans une structure centralisée, en sortie de l'unité de renommage. Elles avaient 5 ports d'émission, qui étaient sous-utilisés en pratique. Niveau ALU, on trouve deux ALUs entières, une flottante, une unité pour les instructions SSE et autres, et trois unités pour les accès mémoire (regroupées en une seule unité dans le schéma ci-dessous). Les unités mémoire regroupent une unité de calcul d'adresse pour les lectures, une autre pour les écritures, et une unité pour la gestion des données à écrire. Les unités de calcul d'adresse sont des additionneurs à 4 opérandes, complétement différents des ALU entières. Les ALU entières sont deux unités asymétriques : une ALU simple, et une ALU complexe incorporant un multiplieur. Les deux peuvent exécuter des opérations d'addition, soustraction, comparaison, etc. [[File:P6 func diag.png|centre|vignette|upright=2|P6 func diag]] Les premiers Pentium 3 n'avaient pas de cache L2 dans le processeur, celui-ci était sur la carte mère. Mais il a été intégré dans le processeur sur la seconde version du Pentium 3, la version Coppermine. Le Pentium 3 a servi de base aux microarchitectures d'Intel qui ont suivi. Les changements à chaque nouvelle génération sont assez mineurs : la prédiction de branchement est améliorée, la taille des stations de réservation et du ROB augmente, idem avec les autres structures liées à l'exécution dans le désordre. Les processeurs Intel ont conservé une fenêtre d'instruction centralisée, alors qu'AMD utilise une autre méthode, comme nous allons le voir dans ce qui suit. Les seuls changements notables sont est le passage à un renommage dans le ROB à un renommage à banc de registre physique, ainsi que l'introduction du cache de micro-opération. Et ce sont des modifications qu'AMD a aussi faites, celle-ci étant clairement une bonne idée pour toutes les micro-architectures avec un budget en transistor suffisant. Il est intéressant de garder cela en tête, car une bonne partie des améliorations de chaque micro-architecture proviendra de là. ===La microarchitecture Core=== La '''microarchitecture Core''' fait suite au Pentium 4, mais reprend en fait beaucoup d’éléments du Pentium 2 et 3. Elle utilise la station de réservation unique avec renommage dans le ROB, provenant du Pentium 2/3. Elle supporte aussi les optimisations des opérations ''load-up'', avec notamment un support des macro-opérations mentionnées plus haut. Les améliorations sont assez diverses, mais aussi assez mineures. * Le processeur incorpore un cache L2, en plus des caches L1 déjà présents auparavant. * La prédiction de branchement a été améliorée avec notamment l'ajout d'une ''Fetch Input Queue''. * L'architecture Core passe à la quadruple émission, soit une instruction de plus que sur le Pentium 2 et 3. Pour cela, un quatrième décodeur est ajouté, il s'agit d'un décodeur simple qui ne fournit qu'une seule micro-opération en sortie. * Un ''stack engine'' et un ''Loop Stream Detector'' ont été ajoutés, ainsi que le support de la macro-fusion qui fusionne une instruction de test et le branchement qui suit en une seule micro-opération. * Les techniques de désambiguïsation mémoire sont implémentées sur cette micro-architecture. Il y a quelques modifications au niveau de l'unité de chargement. La file d'instruction a toujours ce système de fusion de blocs, sauf que les branchements ne causent plus de délai d'un cycle lors du chargement. La file d'instruction est suivie par un circuit de prédécodage qui détermine la taille des instructions et leurs frontières, avant de mémoriser le tout dans une file de 40 instructions. La station de réservation dispose de 6 ports d'émission, mais on devrait plutôt dire 5. Sur les 5, il y en a un pour les lectures, un pour les écritures. Les deux sont reliées à une ''Load/Store Queue'', appelée ''Memory Ordering Buffer''. Elle est elle-même reliée au cache de données par deux ports : un port de lecture et un port d'écriture. Les trois ports d'émission restants sont connectés aux unités de calcul. Les trois ports récents sont chacun reliés à une ALU entière. L'additionneur flottant est connecté au port 1, alors que le multiplieur/diviseur flottante est connecté au port 0. Le fait de mettre les deux sur des ports différents permet d'émettre une addition et une multiplication flottant simultanément. Le multiplieur entier est relié au second port d'émission, celui sur lequel se trouve l'additionneur flottant. Le résultat que le processeur peut émettre un mix d'opérations flottantes et entière assez varié. [[Image:Intel Core2 arch.svg|centre|vignette|upright=2|Intel Core microarchitecture]] ===Les microarchitectures Sandy Bridge and Ivy Bridge=== Les micro-architectures suivant la micro-architecture Core ont introduit quelques grandes modifications : le passage à un renommage à banc de registre physique, l'ajout d'un cache de micro-opérations (et d'un ''Loop Stream Detector''). L'ajout du cache de micro-opérations est un gros changement, particulièrement avec le jeu d’instruction x86. Le décodage des instructions est lent, couteux en énergie. Mais avec l'introduction du cache de micro-opération, la majorité des micro-opérations est non pas décodée, mais lue depuis le cache de micro-opérations. Les décodeurs décodent les instructions pas encore exécutées, mais les exécutions suivantes sont lues depuis le cache de micro-opérations. Et vu la grande présence de boucles, le cache de micro-opérations est l'alimentation principale du pipeline. Les décodeurs servent surtout à alimenter le cache de micro-opérations, parfois décoder quelques instructions isolées exécutées de-dehors de boucles, pas plus. Concrètement, ils servent pour 10 à 20% des micro-opérations exécutées. Intel a d'ailleurs reflété ce fait dans sa terminologie. Intel distingue deux voies de chargement : le ''legacy pipeline'' et le cache de micro-opérations. L'unité de chargement et les décodeurs sont regroupés dans la voie du ''legacy pipeline''. Le cache de micro-opérations est complété avec un ''Loop Stream Detector'', placé après le cache en question. Les décodeurs et le cache de micro-opérations alimentent une file de micro-opérations, située juste avant l'étage de renommage de registres. La file de micro-opérations sert en quelque sorte de tampon entre l'étage de "décodage" et celui de renommage. Le ''Loop Stream Detector'' utilise cette file de micro-opérations comme d'un cache lorsqu'une boucle est détectée. Les micro-opérations de la boucle sont lue depuis la file de micro-opérations, pour être envoyée au renommeur de registres. L'avantage est que le cache de micro-opérations et/ou les décodeurs sont mis en pause et clock-gatés lorsqu'une boucle s'exécute, ce qui réduit la consommation du processeur. Le ''Loop Stream Detector'' et le cache de micro-opération ont globalement le même effet : désactiver tout ce qui est avant, le ''Loop Stream Detector'' appliquant cette méthode au cache de micro-opération lui-même.. Voyons maintenant quelles sont les micro-architectures qui implémentent ces optimisations. Les microarchitectures '''Sandy Bridge''' and '''Ivy Bridge''' sont similaires à l'architecture Core, si ce n'est pour le passage à un renommage à banc de registre physique, et l'ajout d'un cache de micro-opérations. Le nombre de ports d'émission passe à 7, avec 4 pour les instructions arithmétiques (flottantes comme entière), 2 pour les lectures, et un pour les écritures (en fait deux, avec un pour le calcul d'adresse, l'autre pour la donnée à écrire). Pour le reste, rien ne change si ce n'est la prédiction de branchement Les architectures '''Haswell''' et '''Broadwell''' ont ajouté quelques unités de calcul, élargit la sortie du cache de micro-opérations. Un port d'émission pour opération entières a été ajouté, de même qu'un port pour les accès mémoire. Le processeur passe donc à 8 ports d'émission, ce qui permet d'émettre jusqu'à 8 micro-opérations, à condition que le cache de micro-opération suive. Pour le reste, le processeur est similaire aux architectures précédentes, si ce n'est que certaines structures grossissent. L'architecture '''Skylake''' réorganise les unités de calcul et les ports d'émission pour gagner en efficacité. Pour le reste, les améliorations sont mineures. A la rigueur, l'unité de renommage de registre ajoute des optimisations comme l'élimination des MOV, les idiomes liés aux opérations avec zéro, etc. ===Les microarchitectures récentes d'Intel=== Les architectures '''Ice Lake''' et '''Tiger Lake''' passent de quadruple émission à la pentuple émission. Par contre, le processeur utilise toujours 4 décodeurs. Mais les micro-opérations étant émises depuis le cache de micro-opérations, ce n'est pas un problème pour la pentuple émission. Le processeur peut parfaitement émettre 5 micro-opérations en même temps, si elles sont lues depuis le cache de micro-opérations. Là encore, on voit à quel point le cache de micro-opération découple ce qu'il y avant de ce qu'il y a après. La microarchitecture Golden Cove, la plus récente, altère les décodeurs et l'unité de chargement. Sur toutes les générations précédentes, on reste sur une unité de chargement qui charge 16 octets à la fois et il y a toujours 4 décodeurs identiques aux générations précédentes. Golden Cove passe à 6 décodeurs simples, et double la taille du chargement qui passe à 32 octets. Une telle stagnation sur les unités de chargement et de décodage s'explique encore une fois par la présence du cache de micro-opération fait que ce n'est pas trop un problème. Tout ce qui précède le cache de micro-opérations n'a pas de raison d'évoluer, car ce cache est très puissant. ==Un étude des microarchitectures superscalaires x86 d'AMD== Les architectures Intel ont évolué progressivement, sans grandes cassure. Il y a une continuité presque initerrompue entre l'architecture du Pentium 2 et les architectures modernes. Intel a fait des améliorations mineures à chaque nouvelle micro-architecture, si on omet le passage à un renommage à banc de registre physique et l'ajout du cache de micro-opération. A l'opposé, les architectures AMD ont eu de nombreuses cassures dans la continuité où AMD a revu sa copie de fond en comble. Étudier ces architectures demande de voir trois choses séparément : le ''front-end'' qui regroupe l'unité de chargement et les décodeurs, le ''back-end'' qui gère l'exécution dans le désordre et les unités de calcul, et le sous-système mémoire avec les caches et la ''Load Store Queue''. Leur étude sera plus ou moins séparée dans ce qui suit, pour chaque classe d'architecture. ===La première génération de CPU AMD : les architectures K5, K6, K7, K8 et K10=== La première génération de processeurs AMD est celle des architectures K5, K6, K7, K8 et K10. Il n'y a pas de K9, qui a été abandonné en cours de développement. Les processeurs K5 et K6 portent ce nom au niveau commercial. Par contre, les processeurs d'architecture K7 sont aussi connus sous le nom d''''AMD Athlon''', les AMD K8 sont connus sous le nom d''''AMD Athlon 64''', et les architecture K10 sont appelées les '''AMD Phenom'''. Comme le nom l'indique, l'architecture K8 a introduit le 64 bits chez les processeurs AMD. Elles ont une architecture assez similaire pour ce qui est du chargement et des caches. Toutes disposent d'au minimum un cache L1 d'instruction et d'un cache L1 de données. Le K5 n'avait que ces caches, mais un cache L2 a été ajouté avec le K7, puis un L3 avec le K10. L'AMD K5 avait une TLB unique, mais les processeurs suivants avaient une TLB pour le L1 d'instruction et une autre pour le L1 de données. Idem pour le cache L2, avec deux TLB : une pour les données, une pour les instructions. Les caches L1/L2 sont de type exclusifs, à savoir que les données dans le L1 ne sont pas recopiées dans le L2. Le cache L2 est précisément un cache de victime, qui mémorise les données/instructions, évincées des caches L1 lors du remplacement des lignes de cache. L'introduction du cache L2 a entrainé l'ajout de deux TLB de second niveau : une L2 TLB pour les données et une autre pour les instructions. Les architectures K8 et K10 ont ajouté un cache L3, avec un accès indirect à travers l'interface avec le bus. : L'AMD K7 originel, aussi appelée Athlon classique, n'avait pas de cache L2, mais celui-ci était placé sur la carte mère et fonctionnait à une fréquence moitié moindre de celle du CPU. L'Athlon Thunderbird, puis l'Athlon XP, ont intégré le cache L2 dans le processeur. {|class="wikitable" |- ! Architecture AMD ! colspan="5" | Caches |- | rowspan="2" | K5 | L1 instruction || L1 données || colspan="3" | |- | colspan="2" | TLB unique || colspan="3" | |- | colspan="4" | |- | rowspan="2" | K6 | L1 instruction || L1 données || colspan="3" | L2 unifié |- | TLB L1 instruction || TLB L1 données || colspan="3" | |- | colspan="6" | |- | rowspan="2" | K7, K8 | L1 instruction || L1 données || colspan="2" | L2 unifié || |- | TLB L1 instruction || TLB L1 données || TLB L2 instruction || TLB L2 données || |- | colspan="6" | |- | rowspan="2" | K10 | L1 instruction || L1 données || colspan="2" | L2 unifié || L3 |- | TLB L1 instruction || TLB L1 données || TLB L2 instruction || TLB L2 données || |} Fait important, les architectures K5 à K10 utilisent la technique du '''prédécodage''', où les instructions sont partiellement décodées avant d'entrer dans le cache d'instruction. Le prédécodage facilite grandement le travail des décodeurs d'instruction proprement dit. Par contre, le prédécodage prend de la place dans le cache L1 d'instruction, une partie de sa capacité est utilisé pour mémoriser les informations prédécodées. C'est donc un compromis entre taille du cache et taille/rapidité des décodeurs d'instruction. Sur les architectures K5 et K6, le prédécodage précise, pour chaque octet, si c'est le début ou la fin d'une instruction, si c'est un octet d'opcode, en combien de micro-opérations sera décodée l'instruction, etc. A partir de l'AMD K7, le prédécodage reconnait les branchements inconditionnels. Lorsqu'un branchement inconditionnel est pré-décodé, le pré-décodage tient compte du branchement et continue le pré-décodage des instructions à partir de la destination du branchement. Le système de prédécodage est abandonnée à partir de l'architecture Bulldozer, qui suit l'architecture K10. La prédiction de branchement de ces CPU tire partie de ce système de pré-décodage, à savoir que les prédictions de branchement sont partiellement mémorisées dans les lignes de cache du L1 d'instruction. Par exemple, l'AMD K5 se passe de ''Branch Target Buffer'' grâce à cela. Si une ligne de cache contient un branchement, elle mémorise l'adresse de destination de ce branchement, en plus des bits de pré-décodage. Si il y a plusieurs branchements dans une ligne de cache, c'est l'adresse de destination du premier branchement pris dans cette ligne de cache qui est mémoirsée. Un défaut de cette approche est que si le branchement n'est pas dans le L1 d'instruction, aucune prédiction de branchement ne peut être faite et le préchargement ne peut pas fonctionner. C'est une limitation que n'ont pas les BTB découplées du cache L1 : elles peuvent prédire un branchement qui a été évincé dans le L2 ou le L3, tant que l'entrée associée est dans le BTB. Les prédictions peuvent même servir à précharger les instructions utiles. [[File:Comparaison du chargement de l'AMD K5 et K6.png|centre|vignette|upright=2|Comparaison du chargement de l'AMD K5 et K6]] Au niveau du décodage, on trouve de nombreuses différences entre les premières architectures AMD. L'AMD K5 contient 4 décodeurs hybrides, afin de décoder 4 instructions par cycles. Le K5 a quatre décodeurs simples couplés à 4 décodeurs complexes avec chacun un accès au micro-code. Une instruction peut donc passer par a donc deux voies de décodage : un décodage rapide et simple pour les instructions simples, un décodage lent et passant par le microcode pour les instructions complexes. Pour décoder 4 instructions, les deux voies sont dupliquées en 4 exemplaires, ce qui a un cout en circuits non-négligeable. L'AMD K6 utilise moins de décodeurs et ne peut que décoder deux instructions à la fois maximum. Par contre, il fournit en sortie 4 micro-opérations. Il intègre pour cela deux décodeurs simples, un décodeur complexe et un décodeur micro-codé. Un décodeur simple transforme une instruction simple en une ou deux micro-opérations. Il est possible d'utiliser les deux décodeurs simples en même temps, afin de fournir 4 micro-opérations en sortie du décodeur. Les deux autres décodent une instruction complexe en 1 à 4 micro-opérations. Si jamais la ou les deux instructions sont décodées en 1, 2 ou 3 micro-opérations, les micro-opérations manquantes pour atteindre 4 sont remplies par des NOPs. Pour le K7 et au-delà, le processeur dispose de décodeurs séparées pour les instructions micro-codées de celles qui ne le sont pas. Le processeur peut décoder jusqu’à 3 instructions par cycle. Le décodage d'une instruction microcodée ne peut pas se faire en parallèle du décodage non-microcodé. C'est soit le décodeur microcodé qui est utilisé, soit les décodeurs câblés, pas les deux en même temps. Le décodage d'une instruction prend 4 cycles. Les instructions non-microcodées sont décodées en une seule micro-opération, à un détail près : le CPU optimise la prise en charge des instructions ''load-up''. La différence entre le K6 et le K7 s'explique par des optimisations des instructions ''load-up''. Sur le K6, les instructions ''load-up'' sont décodées en deux micro-opération : la lecture en RAM, l'opération proprement dite. Mais sur le K7, une instruction ''load-up'' est décodée en une seule micro-opération. En conséquence, les décodeurs simples sont fortement simplifiés et le décodeur complexe disparait au profit d'un microcode unique. [[File:Décodage sur le K5 et le K5.png|centre|vignette|upright=3|Décodage sur le K5 et le K5]] ====Les microarchitectures K5 et K6 d'AMD==== Les deux premières architectures étaient les architectures K5 et K6, l'architecture K6 ayant été déclinée en quatre versions, nommées K6-1, K6-2, et K-3, avec une version K6-3 bis. Elles sont regroupées ensemble car elles ont beaucoup de points communs. Par exemple, tout ce qui a trait au chargement et au cache était similaire, de même que les unités de calcul. Les deux architectures avaient n'avaient pas de cache L2 et devaient se contenter d'un cache L1 d'instruction et d'un cache L1 de données. L'AMD K5 incorpore une TLB unique, alors que le K6 utilise des TLB séparées pour le cache d'instruction et le cache de données. Une différence entre l'architecture K5 et K6 est que la première utilise des caches normaux, alors que la seconde utilise des ''sector caches''. Les deux architectures disposaient des unités de calcul suivantes : deux ALU entières, une FPU, deux unités LOAD/STORE pour les accès mémoire, une unité de branchement et une ou plusieurs unités SIMD. Une organisation classique, donc. Pour les unités entières, il y avait deux ALU simples, un ''barrel shifter'' et un diviseur. Il n'y a pas d'erreur, le processeur incorpore un circuit diviseur, mais pas de circuit multiplieur. La raison est que la multiplication est réalisée par la FPU ! En effet, le multiplieur flottant de la FPU intègre un multiplieur entier pour multiplier les mantisses, qui est utilisé pour les multiplications entières. La même technique a été utilisée sur l'Atom, comme vu plus haut. Le tout était alimenté par deux ports d'émission, appelés ports X et Y. Sur l'architecture K5, le ''barrel shifter'' et le diviseur sont des ports différents. {|class="wikitable" |+ AMD K5 |- ! Port X ! Port Y |- | ALU simple | ALU simple |- | ''Barrel Shifter'' | Diviseur |} Sur l'architecture K6, le ''barrel shifter'' et le diviseur sont sur le même port. {|class="wikitable" |+ AMD K6 |- ! Port X ! Port Y |- | ALU simple | ALU simple |- | | ''Barrel Shifter'' |- | | Diviseur |} Niveau unités mémoire, le K5 avait deux unités LOAD/STORE, chacune capable de faire lecture et écriture. Par contre, la ''store queue'' n'a qu'un seul port d'entrée, ce qui fait que le processeur peut seulement accepter une écriture par cycle. Le processeur peut donc émettre soit deux lectures simultanées, soit une lecture accompagnée d'une écriture. Impossible d'émettre deux écritures simultanées, ce qui est de toute façon très rare. L'architecture K6 utilise quant à elle une unité LOAD pour les lectures et une unité STORE pour les écritures. Ce qui permet de faire une lecture et une écriture par cycle, pas autre chose. Niveau unités SIMD, l'architecture K7 n'avait qu'une seule unité SIMD, placée sur le port d'émission X. L'architecture K8 ajouta une seconde unité SIMD, sur l'autre port d'émission entier. De plus, trois ALU SIMD ont été ajoutées : un décaleur MMX, une unité 3DNow!, une unité mixte MMX/3DNow. Elles sont reliées aux deux ports d'émission entier X et Y ! Elles ne sont pas représentées ci-dessous, par souci de simplicité. [[File:Unité de calcul des processeurs AMD K5 et K6.png|centre|vignette|upright=2|Unité de calcul des processeurs AMD K5 et K6. les unités sur la même colonnes sont reliées au même port d'émission.]] Si les unités de calcul et le chargement sont globalement les mêmes, les deux architectures se différencient sur l'exécution dans le désordre. L'AMD K5 utilise du renommage de registre dans le ROB avec des stations de réservation. Par contre, l'AMD K6 utilise une fenêtre d'instruction centralisée. De plus, son renommage de registre se fait avec un banc de registre physique. L'architecture AMD K5 utilisait de deux stations de réservation par unité de calcul, sauf pour les deux unités mémoire partageaient une station de réservation unique (deux fois plus grande). Les stations de réservation sont cependant mal nommées, vu que ce sont en réalité des mémoire FIFO. Une micro-opération n'est émise que si elle est la plus ancienne dans la FIFO/station de réservation. Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique. Le tampon de ré-ordonnancement faisait seulement 16 instructions. [[File:AMD K5.jpg|centre|vignette|upright=3|AMDK5 Diagramme.]] L'architecture K6 remplace les stations de réservations par une fenêtre d'instruction centralisée. Les 4 micro-opérations renommées sont écrites dans la fenêtre d'instruction par groupe de 4, NOP de ''padding'' inclus. La fenêtre d'instruction centralisé contient 24 micro-opérations, groupées en 6 groupes de 4 micro-opérations, avec potentiellement des NOP dedans suivant le résultat du décodage. L'avantage est que l'implémentation de la fenêtre d'instruction est simple. La fenêtre d'instruction centralisée permettait d'émettre 6 micro-opérations en même temps (une par unité de calcul/mémoire). Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique. Le processeur utilisait un renommage avec un banc de registre physique. Le banc de registre physique pour les entiers contenait 48 registres, dont 24 étaient des registres architecturaux et 24 étaient des registres renommés. Sur les 24 registres architecturaux, 16 avaient une fonction de ''scratchpad'' que les ''datasheets'' d'AMD ne détaillent pas, les 8 restants étaient les registres généraux EAX, EBX, etc. [[File:AMD K6 Little foot & Modl 6.png|centre|vignette|upright=3|AMD K6 original.]] ====Les microarchitectures K7, K8 et K10 d'AMD==== Les microarchitectures suivantes sont les architectures K7, K8 et K10. Les architectures K7, K8 et K10 sont assez similaires. La différence principale entre le K7 et le K8 est le support du 64 bits. Les apports du K10 sont la présence d'un cache L3, d'une unité de calcul supplémentaire et d'améliorations de la prédiction de branchement. La taille de certains caches a été augmentée, de même que la largeur de certaines interconnexions/bus. L'architecture K7 des processeurs Athlon utilisait le renommage de registre, mais seulement pour les registres flottants, pas pour les registres entiers. Le ranommeg des registres flottants étaient réalisé via un banc de registres physique, ne contenant que des registres flottants. Les architectures K8 et K10 utilisent le renommage de registres pour tous les registres, entiers comme flottants. Par contre, le renommage de registre n'est pas réalisé de la même manière pour les registres entiers et flottants. Les registres entiers sont renommés dans le tampon de ré-ordonnancement, comme c'était le cas sur les architectures Intel avant le Pentium 4. Par contre, les registres flottants sont renommés grâce à un banc de registre physique. Le K8 est donc un processeur au renommage hybride, qui utilise les deux solutions de renommage principales. A partir du K7, le CPU optimise la prise en charge des instructions ''load-up''. Les instructions ''load-op'' sont appelées des macro-opérations dans la terminologie d'AMD, et aussi d'Intel. L'idée est que les instructions ''load-up'' sont décodées en micro-opérations intermédiaires. Elles sont propagées dans le pipeline comme étant une seule micro-opération, jusqu'à l'étage d'émission. Lors de l'émission, les instructions ''load-up'' sont scindées en deux micro-opérations : la lecture de l'opérande, puis l'opération proprement dite. Faire ainsi économise des ressources et optimise le remplissage du tampon de ré-ordonnancement, des fenêtres d'instructions, des stations de réservation, etc. Le tampon de réordonnancement est combiné avec divers circuits en charge de l'exécution dans le désordre, dans ce qui s'appelle l'''instruction control unit''. Il contient de 72 à, 84 instructions, qui sont regroupées en groupes de 3. Là encore, comme pour le K5 et le K6, le tampon de réordonnancement tient compte de la sortie des décodeurs. Les décodeurs fournissent toujours trois micro-opérations par cycle, quitte à remplir les vides par des NOP. Le tampon de réordonnancement reçoit les micro-opérations, NOP inclus, par groupes de 3, et est structuré autour de ces triplets de micro-opération, y compris en interne. Pour ce qui est de l'unité mémoire, elle est précédée par une file de µops mémoire, qui émet les accès mémoire dans l'ordre du programme. Elle est souvent qualifiée de ''Load-Store Queue'', mais ce n'est pas la terminologie que nous utilisons dans ce cours. La file de micro-opération lire/écrire 64 bits par cycle depuis le cache L1, ce qui fait un seul accès au cache par cycle. La file de µops mémoire est appelée la ''Pre-Cache Queue''. Si au vu de son nom, vous avez deviné qu'il y avait une ''Post-Cache Queue''. Elle mémorise les lectures/écritures émises, mais qui ont levé un défaut de cache L1. Elle ne fait pas partie de la file de µops mémoire proprement dite. Les architectures K7, K8 et K10 ont des unités de calcul très similaires. Concrètement, il y a trois ALU entières, trois unités de calcul d'adresse, et une FPU. Le processeur incorpore, aussi un multiplieur entier, relié sur le port d'émission de la première ALU. La FPU regroupe un additionneur flottant, un multiplieur flottant, et une troisième unité LOAD/STORE pour les lectures/écritures pour les nombres flottants. L'architecture K8 ajoute une unité de manipulation de bit, la K10 un diviseur entier. [[File:Unité de calcul des processeurs AMD K7, K8 et K10.png|centre|vignette|upright=2|Unité de calcul des processeurs AMD K7, K8 et K10]] La manière d'alimenter les ALU en micro-opérations varie un petit peu entre les architectures K7, K8 et K10. Il y a cependant quelques constantes entre les trois. La première est qu'il y a une fenêtre d'instruction séparée pour les flottants, de 36 à 42 entrées, avec renommage de registre. La fenêtre d'instruction flottante a trois ports d'émission : un pour l'additionneur flottant, un autre pour le multiplieur, et un troisième pour la troisième unité flottante qui s'occupe du reste. La seconde est que chaque ALU entière est couplée avec une unité de calcul d'adresse. Par contre, la méthode de couplage varie d'un processeur à l'autre. : Les stations de réservation sont nommées des ''schedulers'' dans les schémas qui suivent. La microarchitecture K7 avait deux fenêtres d'instruction : une pour les opérations flottantes, une autre pour les instructions entières et les accès mémoire. La fenêtre d'instruction entière était reliée à 3 ALU entières et à 3 AGU. Elle pouvait émettre trois micro-opérations en même temps : trois micro-opérations entières, trois micro-opérations mémoire. Les AGU étaient reliées à la file de µops mémoire mentionnée plus haut, ce qui permet d'émettre trois µops mémoire par cycle. Par contre, la file de µops mémoire ne pouvait exécuter qu'une lecture de 64 bits ou une écriture de 64 bits. En clair, trois micro-opérations mémoire peuvent être émises par cycle, cela entraine trois calculs d'adresse simultanés, mais les trois lectures/écritures sont mises en attente dans la file de µops mémoire. Elles s'exécutent alors l'une après l'autre. La fenêtre d'instruction entière contenait 5 à 6 groupes de 3 macro-opérations. Vous noterez que j'ai parlé de macro-opérations et pas de micro-opérations, car les instructions ''load-up'' sont considérées comme une seule "micro-opération" dans la fenêtre d'instruction entière. Et cela se marie bien avec une fenêtre d'instruction unique partagée entre pipeline entier et pipeline mémoire. Une macro-opération était scindée en deux micro-opérations : une micro-opération mémoire et une micro-opération entière. Il est donc avantageux de regrouper unités mémoire et unités entières à la même fenêtre d'instruction pour ce faire. [[File:AMD K7.png|centre|vignette|upright=3|AMD K7]] Sur les architectures K8 et K10, la station de réservation unique de 15 micro-opérations est remplacée par trois stations de réservations, de 8 micro-opérations chacune pour le K8, de 10 pour le K10. Chaque station de réservation entière alimente une unité de calcul entière et une unité de calcul d'adresse. l'unité de calcul d'adresse est reliée à la file de µops mémoire, qui n’exécute toujours qu'un seul accès mémoire par cycle. Le multiplieur est relié à la première station de réservation, sur le même port d'émission que l'ALU. [[File:AMD Husky microarchitecture.png|centre|vignette|upright=3|AMD Husky microarchitecture]] La microarchitecture K10 a été déclinée en plusieurs versions, nommées Grayhound, Grayhound+ et Husky, Husky étant une architecture gravée en 32 nm dédiée aux processeurs A-3000. L'architecture Grayhound a plus de cache et un ROB plus grand, la Husky est quand à elle un peu plus différente. Elle n'a pas de cache L3, contrairement aux autres architectures K10, ce qui simplifie fortement son sous-système mémoire. Par contre, les fenêtres d'instructions/stations de réservation et le ROB sont plus grands, pareil pour les files dans l'unité mémoire. Une ALU pour les divisions entières a aussi été ajoutée. Pour résumer, les architectures K7, K8 et K10 séparent les pipelines entiers et flottants : trois pipelines entiers avec chacun son unité de calcul, et un pipeline flottant avec plusieurs unités de calcul. Les raisons à cela sont assez diverses. Disons que dupliquer des ALU entières simples prend peu de transistors, là où les gros circuits comme le multiplieur ou la FPU ne sont pas dupliqués. Et cela a un autre avantage : le renommage, ''dispatch'' et l'émission sont plus simples. Les pipelines entiers ont une exécution dans le désordre peu complexe, grâce au grand nombre d'unités de calcul, ce qui fait que le pipeline entier est de seulement 15 cycles au total (chargement et décodage inclus). A l'opposé, la FPU est alimentée par une exécution dans le désordre très complexe, avec banc de registre physique et beaucoup de ressources, mais au prix d'un pipeline flottant plus long de 3 cycles, soit 18 cycles au total. ===Les microarchitectures ZEN d'AMD=== Viennent ensuite les '''microarchitectures Bulldozer''', avec trois révisions ultérieures nommées Piledriver, Steamroller et Excavator. Mais du fait de l'utilisation de techniques de multithreading matériel que nous n'avons pas encore abordé, nous ne pouvons pas en parler ici. Les microarchitectures suivantes sont les '''architectures ZEN 1/2/3/4/5'''. Elles se ressemblent beaucoup, chacune accumulant les améliorations des précédentes. Mais le cœur de l'architecture reste plus ou moins le même. En passant à la suivante, le nombre de registre virtuel augmente, le ''branch target buffer'' augmente en taille, le ROB et les files d'attente grossissent, les caches de micro-opération aussi, les caches grossissent, etc. Une optimisation intéressante est l'ajout d'un cache de micro-opération, qui améliore grandement les performances du ''front-end'', notamment pour les boucles. La microarchitecture Zen 1 est illustrée ci-dessous. Comme on le voit, les registres flottants ont une unité de renommage séparée de celle pour les entiers, mais les deux utilisent du renommage à banc de registre physique. Il y a par contre une différence au niveau des fenêtres d'instruction, notées ''scheduler'' dans le schéma. Pour ce qui est des unités de calcul flottantes, il y a une fenêtre unifiée qui alimente quatre ALU, grâce à 4 ports d'émission. Mais pour les ALU entières, il y a une fenêtre d'instruction par ALU, avec un seul port d'émission connecté à une seule ALU. La raison de ce choix est que les opérations flottantes ont un nombre de cycle plus élevé, sans compter que les codes flottants mélangent bien additions et multiplication. Une fois décodées, les instructions sont placées dans une première file de micro-opérations om elles attendent, puis sont dispatchées soit dans le pipeline entier, soit dans le pipeline flottant. les micro-opérations entières sont insérées dans une fenêtre d'instruction directement, alors que les micro-opérations flottantes doivent patienter dans une seconde file de micro-opérations. La raison est que les micro-opérations flottantes ayant une grande latence, trop d'instructions flottantes consécutives pourraient bloquer le pipeline flottant, sa fenêtre d'instruction étant pleine. Le pipeline flottant étant bloqué, la première file de micro-opérations serait bloquée et on ne pourrait plus émettre de micro-opérations entières. Pour éviter cela, une solution serait d'agrandir la file de micro-opérations, mais cela la rendrait plus lente et se ferait donc au détriment de la fréquence d'horloge. Alors une solution a été d'ajouter une seconde file de micro-opérations, au lieu d'agrandir la première. [[File:Zen microarchitecture.svg|centre|vignette|upright=3|Microarchitecture Zen 1 d'AMD.]] Le passage à la microarchitecture n'a pas causé de grands changements. Le Zen 2 a ajouté une unité de calcul d'adresse, ce qui fait qu'on passe à 4 ALU, 3 AGU et 4 FPU. La fenêtre d'instruction flottante reste la même. Par contre, les fenêtres d'instruction entières changent un peu. Ou plutot devrais-je dire les fenêtres d'instruction mémoire. En effet, le Zen 2 fusionne les fenêtres d'instructions liées aux AGU en une seule fenêtre d'instruction deux fois plus grosse. Le Zen 5 a ajouté deux autres ALU entières et une unité de calcul d'adresse (6 ALU / 4 AGU) ==Les processeurs Atom d'Intel, de microarchitecture Bonnell== L'architecture de l'Atom première génération est assez simple. Son pipeline faisait 16 étages, ce qui est beaucoup. C'est un processeur 32 bits, ce qui aura son importance dans ce qui suit. Il était conçu pour être un processeur basse consommation, donc peu puissant. En conséquence, il n'a pas d'exécution dans le désordre, même s'il est superscalaire. C'était la norme à l'époque pour les processeurs basse consommation, que de faire sans exécution dans le désordre. De nos jours, les choses ont bien changée, même les processeurs basse consommation ont exécution dans le désordre, superscalarité et renommage de registres. ===Le ''front-end'' de l'Atom=== Le cache d'instruction permet de lire 8 octets par cycle, qui sont placés dans une file d'instruction, elle-même suivie par deux décodeurs. Le fait que les décodeurs lisent les instructions depuis une file d'instruction fait que les deux instructions décodées ne sont pas forcément consécutives en mémoire RAM. Par exemple, l'Atom peut décoder un branchement prédit comme pris, suivi par l'instruction de destination du branchement. Les deux instructions ont été chargées dans la file d'instruction et sont consécutifs dedans, alors qu'elles ne sont pas consécutives en mémoire RAM. Sur l'Atom, la majorité des instructions x86 sont décodées en une seule micro-opération, y compris les instructions ''load-up''. Le microcode n'est utilisé que pour une extrême minorité d'instructions et est à part des deux décodeurs précédents. L'avantage est que cela permet d'utiliser au mieux la file de micro-opération, qui est de petite taille. Mais surtout, cela permet de grandement réduire la consommation du processeur, au détriment de ses performances. Pour avoir un décodage rapide, malgré des instructions complexes, le processeur recourt à la technique du pré-décodage, qui prédécode les instructions lors de leur chargement dans le cache d'instruction. Le prédécodage lui-même prend deux cycles, là où une lecture dans le L1 d'instruction en prend 3. les défauts de cache d'instruction sont donc plus longs de deux cycles. Mais l'avantage du prédécodage est que la consommation d'énergie est diminuée. Prenez une instruction exécutée plusieurs fois, dans une boucle. Au lieu de décoder intégralement une instruction à chaque fois qu'on l'exécute, on la prédécode une fois, seul le reste du décodage est fait à chaque exécution. D'où un gain d'énergie assez intéressant. Les caches de micro-opération, qui sont capables d'exécuter une optimisation similaire, n'existaient pas encore à cette époque. ===Le chemin de données de l'Atom=== Les deux décodeurs alimentent une file de micro-opérations de petite taille : 32 µops maximum, 16 par ''thread'' si le ''multithreading'' matériel est activé. La file de micro-opérations a deux ports d'émission, ce qui permet d'émettre au maximum 2 µops par cycle. Les conditions pour cela sont cependant drastiques. Les deux instructions ne doivent pas avoir de dépendances de registres, à quelques exceptions près liées au registre d'état. Le multithreading matériel doit aussi être désactivé. Les deux instructions doivent aller chacun dans un port différent, et cela tient en compte du fait que les deux ports sont reliés à des unités de calcul fort différentes. Le tout est illustré ci-dessous. Les deux ports ont chacun une ALU simple dédiée, capable de faire des additions/soustractions, des opérations bit à bit et des copies entre registres. Mais ils ont aussi des opérations qui leur sont spécifiques. La séparation entre les deux pipelines est assez complexe. Il ne s'agit pas du cas simple avec un pipeline entier et un pipeline flottant séparés. En réalité, il y a deux pipelines, chacun capables de faire des opérations entières et flottantes, mais pas les mêmes opérations. Le premier port permet d’exécuter des opérations entières simples, une addition flottante, des comparaisons/branchements, ou une instruction de calcul d'adresse LEA. Le second port/pipeline est, quant à lui, conçu pour exécuter les instruction ''load-up'' nativement, en une seule micro-opération. Il contient toute la machinerie pour faire les accès mémoire, notamment des unités de calcul d'adresse et un cache L1 de données. A la suite du cache, se trouvent une ALU entière simple, un ''barrel shifter'', et un circuit multiplieur/diviseur. Le circuit multiplieur/diviseur est utilisé à la fois pour les opérations flottantes et entières. [[File:Intel Atom Microarchitecture.png|centre|vignette|upright=2.5|Intel Atom Microarchitecture]] Cette organisation difficile à comprendre est en réalité très efficace, très économe en circuit, tout en gardant une performance intéressante. Les instructions simples, ADD/SUB/bitwise sont supportées dans les deux pipelines. Il faut dire que ce sont des opérations courantes qu'il vaut mieux optimiser au mieux. Le processeur peut donc émettre deux opérations simples et fréquentes en même temps, ce qui augmente les performances. Les opérations plus complexes, à savoir les multiplications/divisions/décalages/rotations/manipulations de bit sont supportées dans un seul pipeline. La raison est qu'il est rare que de telles opérations soient consécutives, et qu'il n'est donc pas utile d'optimiser pour cette situation. Si les deux pipelines devaient supporter ces opérations, cela demanderait de dupliquer les circuits multiplieurs/diviseur, ce qui aurait un cout en circuit important pour un gain en performance assez faible. ===Le système d'exceptions flottantes de l'Atom=== Le processeur étant sans exécution dans le désordre, ses instructions doivent écrire dans les registres dans l'ordre du programme. En conséquence, certaines instructions doivent être retardées, leur émission doit attendre que les conditions soient adéquates. Et cela pose problème avec les opérations flottantes, vu qu'elles prennent pas mal de cycles pour s'exécuter. Imaginez qu'une instruction flottante de 10 cycles soit suivie par une instruction entière. En théorie, on doit retarder l'émission de l'instruction entière de 9 cycles pour éviter tout problèmes. Le cout en performance est donc assez important. En théorie, les instructions entières et flottantes écrivant dans des registres séparés, ce qui fait que l'on pourrait exécuter instructions entières et flottantes dans le désordre. Sauf pour les instructions de copie entre registres entier et flottants, mais laissons-les de côté. Le problème est qu'une instruction flottante peut parfois lever une exception, par exemple en cas de division par zéro, ou pour certains calculs précis. Si une exception est levée, alors l'instruction flottante est annulée, de même que toutes les instructions qui suivent, y compris les opérations entières. Ce n'est pas un problème si le processeur gère nativement les exceptions précises, par exemple avec un tampon de ré-ordonnancement. Mais l'Atom étant un processeur sans exécution dans le désordre, les instructions entières devraient être mises en attente tant qu'une instruction flottante est en cours d'exécution. Heureusement, l'Atom d'Intel a trouvé une parade. La technique, appelée ''Safe Instruction Recognition'' par Intel, est décrite dans le brevet US00525721.6A. L'idée est de tester les opérandes flottantes, pour détecter les combinaisons d'opérandes à problème, dont l'addition/multiplication peut lever une exception. Si des opérandes à problème sont détectées, on stoppe l'émission de nouvelles instructions en parallèle de l'instruction flottante et l'unité d'émission émet des bulles de pipeline tant que l'instruction flottante est en cours. Sinon, l'émission multiple fonctionne. La technique permet ainsi de ne pas écrire dans les registres entiers/flottants dans l'ordre du programme : une instruction entière peut être autorisée à s'exécuter même si elle écrit dans un registre entier avant qu'une instruction flottante délivre son résultat. ==La microarchitecture Netburst du Pentium 4== Dans cette section, nous allons voir l'architecture du processeur Pentium 4, qu'on a volontairement laissée de côté précédemment. Pourquoi un tel saut dans le temps ? Parce que le Pentium est complément à part des autres architectures Intel. Le Pentium 4 a représenté une rupture en termes de microarchitecture, qui a été un échec tellement retentissant que les processeurs suivants sont repartis sur la base du Pentium 3. Il introduisait de nombreuses nouveautés architecturales qui étaient très innovantes. Par exemple, il introduisait le renommage avec un banc de registre physique, qui a été utilisé sur tous les processeurs Intel suivants. Mais la plupart de ces innovations étaient en réalité de fausses bonnes idées, ou du moins des idées difficiles à exploiter. Par exemple, le système de pipeline à ''replay'' n'a été utilisé que sur le Pentium 4 et aucun autre processeur ne l'a implémenté. ===Un focus sur la fréquence d'horloge=== La microarchitecture du Pentium 4 a été déclinée en plusieurs versions, dont les finesses de gravure n'étaient pas les mêmes. La microarchitecture Netburst, utilisée sur le Pentium 4, utilisait un pipeline à 20 étage, augmenté à 32 sur une révision ultérieure. Il a existé quatre révisions de l'architecture : Willamette (180 nm), Northwood (130 nm), Prescott (90 nm) et Cedar Mill (65 nm). Un point important est que le Pentium 4 était prévu pour fonctionner à haute fréquence. Ses 1,5 GHz étaient impressionnants pour l'époque, les autres processeurs tournant à une fréquence proche du GigaHertzs. Pour cela, la solution retenue par Intel a été un pipeline très long, avec beaucoup d'étages. Le Pentium 4 a été décliné en plusieurs versions assez proches, chacune avec sa propre finesse de gravure qui n'ont pas toute le même pipeline. Les micro-architectures ''Willamette'' et ''Northwood'' avaient un pipeline de 20 étages, alors que les autres processeurs de l'époque avaient entre 10 et 15 étages maximum. Les micro-architectures ''Prescott'' et ''Cedar Mill'' étaient une refonte qui a fait grimper le nombre d'étages à 31 ! Du jamais vu, il s'agit d'un record pour un processeur commercial. Un pipeline aussi long permet d'exécuter beaucoup d’instructions en même temps, chacune dans un étage, mais aussi d'atteindre de hautes fréquences facilement. Le problème est qu'un pipeline avec autant d'étages a beaucoup de problèmes. Un point important est que la prédiction de branchement est cruciale. Pour rappel, la pénalité en cas de mauvaise prédiction dépend du nombre d'étages avant que le branchement soit résolu. Et les branchements sont résolus soit en fin de décodage, soit dans l'unité de calcul. C'est à dire au milieu du pipeline, soit en fin de pipeline. La pénalité en cas de mauvaise prédiction de branchement était énorme sur le Pentium 4, elle atteignait facilement 30 cycles Pour compenser, le Pentium 4 avait une prédiction de branchement très performante, pour l'époque. J'insiste sur le pour l'époque. Il utilisait un prédicteur qu'on a déjà abordé dans le chapitre sur la prédiction de branchement, précisément un prédicteur adaptatif à deux niveaux avec un historique global de 16 bits. Il avait aussi un ''Branch Target Buffer'' de 4096 entrées. Mais surtout, il intégrait une sorte de précurseur du cache de micro-opération, appelé le cache de traces, qui est détaillé dans la section suivante. ===Le cache de trace du Pentium 4=== Les décodeurs du Pentium 4 ne font pas décoder les instructions, ils mémorisent le résultat dans un cache de micro-opération un peu particulier, appelé le '''cache de trace'''. Une ligne de cache peut mémoriser 6 micro-opérations, ce qui peu sembler peu mais a été repris sur les micro-architectures suivantes. Mais le cache de trace a une grande différence avec un cache de micro-opération normal. Un cache de micro-opération normal mémorise une instruction par ligne de cache. Une instruction est décodée en plusieurs micro-instructions, qui sont enregistrées dans une ligne de cache. Si l'instruction n'utilise par les 6 micro-opérations disponibles, le reste de la ligne de cache n'est pas utilisé. Mais le Pentium 4 optimise le tout de manière ce à ce que ne soit pas le cas. Sur le Pentium 4, la contrainte du "une instruction par ligne de cache" est abandonnée. Une ligne de cache mémorise 6 micro-opérations consécutives, qui peuvent appartenir à plusieurs instructions. Par exemple, si le décodeur décode 4 instructions consécutives en 6 micro-opérations au total, alors le tout prendra une seule ligne de cache sur le Pentium 4. Et les 4 instructions consécutives n'ont même pas à être consécutives en mémoire : il peut y avoir des branchements pris entre ces instructions ! {|class="wikitable" |+ Cache de trace |- ! Ligne de cache | ADD || SUB || ADD || MOV || MUL || ''shift'' |- ! Ligne de cache | colspan="3" | ADD ''load-up'' || MUL || colspan="2" | Branch if Equal |- ! Ligne de cache | XOR || colspan="4" | POP || SUB |- ! ... | colspan="6" | ... |} Pour expliquer cela plus concrètement, nous allons devoir introduire les concepts de trace et de bloc de base. Un '''bloc de base''' (''basic block'') est une suite d'instructions sans branchement, qui est séparé par deux branchements. Le début d'un bloc de base est la destination d'un branchement, un bloc de base se termine avec un branchement. Une '''trace''' est formée en concaténant plusieurs blocs de base. Pour donner un exemple, regardez le code illustré ci-contre. Il est composé d'un bloc de base A, suivi par un bloc de base B, qui peut faire appel soit au bloc C, soit un bloc D. Un tel code peut donner deux traces : ABC ou ABD. La trace exécutée dépend du résultat du branchement qui choisit entre C et D. Le cache de trace mémorise des traces de 6 micro-opérations consécutives. Les traces sont formées en sortie des décodeurs d'instruction, par de subtiles opérations mélangeant mémorisation, décalage et concaténation. Les circuits qui construisent les traces ne sont pas connus, mais ils doivent certainement être très compliqués. toujours est-il qu'un cache de trace peut mémoriser des traces différentes, même si leur début est le même. Par exemple, prenons deux traces, composées des blocs de base A, B, C et D. La première trace est la trace ABC, la seconde est la trace ABD. Les deux traces auront chacune une ligne de cache dédiée. Une trace est réutilisable quand le premier bloc de base est identique et que les prédictions de branchement restent identiques. Pour vérifier cela, le tag du cache de traces contient l'adresse du premier bloc de base, la position des branchements dans la trace et le résultat des prédictions utilisées pour construire la trace. Le résultat des prédictions de branchement de la trace est stocké sous la forme d'une suite de bits : si la trace contient n branchements, le n-ième bit vaut 1 si ce branchement a été pris, et 0 sinon. Même chose pour la position des branchements dans la trace : le bit numéro n indique si la n-ième instruction de la trace est un branchement : si c'est le cas, il vaut 1, et 0 sinon. Si la trace est réutilisée par la suite, elle est lue depuis le cache de traces. Pour savoir si une trace est réutilisable, l'unité de chargement envoie le ''program counter'' au cache de traces, l'unité de prédiction de branchement fournit le reste des informations. Si on a un succès de cache de traces, et la trace est envoyée directement au décodeur. Sinon, la trace est chargée depuis le cache d'instructions et assemblée. Il faut signaler que le cache de trace avait sa propre unité de prédiction de branchement séparée de l'unité de prédiction de branchement normale. [[File:TraceCache.png|centre|vignette|upright=2|Cache de traces.]] Le cache de traces réduisait la longueur du pipeline en cas de succès de cache de trace. Quand les instructions étaient lues depuis le cache de trace, les étages avant le cache de trace ne sont pas utilisés, tout se passe comme s'ils étaient retirés du pipeline. C'est la même chose avec le cache de micro-opération des processeurs modernes, mais l'idée n'existait pas encore à l'époque. Le cache de trace mémorise des traces décodées, ce qui fait qu'un succès de cache de trace contournait non seulement le cache d'instruction, mais aussi les décodeurs. Le temps d'accès au cache de trace pouvait être assez élevé, même s'il était comparable au temps d'accès du cache d'instruction. Le cache de traces a depuis été remplacé par une alternative bien plus intéressante, le cache de micro-opérations, plus flexible et plus performant. Comparé à un cache de trace, la contrainte "une instruction par ligne de cache" simplifie grandement l'implémentation d'un cache de micro-opération. Ne parlons pas de la détection des succès de cache, qui demande d'utiliser les prédictions de branchement. Mais le vrai problème avec le cache de trace est tout autre. Il arrive souvent qu'une micro-opération soit présente dans plusieurs lignes de cache en raison du processus de construction des traces, chose impossible avec un cache de micro-opération. Et c'est un problème, qui réduit la capacité effective du cache de trace. Alors certes, une ligne de cache est plus remplie que sur un cache de micro-opération, on est certain que les 6 micro-opération par ligne de cache sont remplies. Mais la redondance réduit grandement cet avantage. ===L'exécution dans le désordre et le chemin de données du P4=== Le renommage de registres se fait avec un banc de registres physiques avec une table d'alias. Le Pentium 4 avait une exécution dans le désordre très limitée, basée sur la présence de deux files de micro-opération : une pour les accès mémoire, une autre pour les autres instructions. La seconde file regroupait opérations entières et flottantes, elles n'étaient pas séparées. Avec ces deux files, les instructions mémoire étaient exécutées dans l'ordre du programme, les instructions arithmétiques s'exécutaient aussi dans l'ordre du programme, mais une instruction arithmétique pouvait passer avant une instruction mémoire et inversement. L'avantage est que cela permettait de faire des lectures en avance, c'était une forme limitée de lecture non-bloquantes. Il s'agit bel et bien de deux files d'instructions, pas de fenêtres d'instruction ni de stations de réservation. Le Pentium 4 est le seul processeur commercial qui a utilisé des files de micro-opération séparées, tous les autres utilisent des fenêtres d'instruction : centralisées pour Intel, décentralisées pour AMD (en général). Le Pentium 2 et 3, bien qu'antérieurs, utilisait une station de réservation unique. Cela peut sembler être un retour en arrière, mais les files d'instructions sont bien plus larges : de 42 micro-opérations pour le Pentium 3, on passe à 120 micro-opérations pour le Pentium 4. Et vu la longueur du pipeline, qui fait qu'il y a plus d'instructions en vol, c'était une nécessité. Mais cela n'aurait pas été possible en utilisant des stations de réservation, pour des raisons de consommation électrique et/ou de budget en transistors, ce qui fait que passer à une file de micro-opération été la solution retenue. Pour les accès mémoire, le Pentium 4 utilisait donc une file de µops mémoire unique, couplée à une file d'écriture (non-représentée sur les schémas qui suivent). La file d'écriture du Pentium 4 était de 24 écritures maximum et gérait le ''Store-to-load forwarding''. Le processeur pouvait émettre une lecture et une écriture à chaque cycle. Il y avait un port d'émission pour les lectures et un autre pour les écritures, tous deux ayant chacun leur propre unité de calcul d'adresse. Le processeur contenait 3 ALU entières, 2 unités de calcul d'adresse et une FPU. La FPU était complétée par une unité pour faire des copies entre registres flottants, des opérations MOV. Pour les AGU, il y en avait une dédiée aux lectures, une autre pour les écritures. Le tout était relié aux ports d'émissions comme suit : [[File:Ports d'émission du Pentium 4.png|centre|vignette|upright=2.5|Ports d'émission du Pentium 4]] Le processeur utilisait deux réseaux de contournement séparés : un pour les opérations flottantes, un pour les opérations entières. Le réseau de contournement pour les opérations entières est aussi relié aux unités de calcul d'adresse. Jusque là, rien de surprenant, le chemin de données du processeur est assez classique. [[File:Architettura Pentium 4.png|centre|vignette|upright=3|Microarchitecture du Pentium 4.]] ===Les unités de calcul entières du Pentium 4=== Sur le Pentium 4, les ALU entières étaient cadencées à une fréquence double de celle du processeur. Les ALU entières pouvaient exécuter deux micro-opérations par cycle, ce qui fait que les ports d'émissions reliés aux ALU devaient eux aussi fonctionner à double fréquence. Pour faire la différence entre les deux fréquences, nous parlerons de fréquence/cycle processeur et de fréquence/cycle de l'ALU. Précisons que seules les ALU entières étaient à double fréquence, pas le multiplieur, pas le ''barrel shifter''. Pour simplifier, nous allons parler d'additionneur plutôt que de l'ALU entière, ce qui sera plus proche de la réalité. Et l'implémentation de l'additionneur du Pentium 4 était très innovante. L'additionneur pouvait exécuter deux additions par cycle, même si les deux additions ont une dépendance. Mais n'allez pas croire que l'implémentation était intuitive, avec un additionneur 32 bit basique très rapide. Non seulement l'additionneur fonctionnait à double fréquence, mais il était aussi pipeliné, avec un système de contournement interne ! Les additionneurs étaient pipelinées, d'une manière très simple. Une addition 32 bits était découpée en trois étapes : deux additions de 16 bits, une dernière étape pour mettre à jour le registre d'état. Pour cela, chaque additionneur était composé de deux additionneur 16 bits chacune, placées l'une après l'autre, avec un registre de pipeline entre les deux. L'additionneur prenait deux cycles d'horloge pour faire son travail : le premier cycle calculait les 16 bits de poids faible, le second calculait les 16 bits de poids fort lors du second cycle. Le tout est appelé '''addition étagée''' (''staggered add'') dans la documentation Intel. Une addition se fait donc en deux étapes, sauf que c'est compensé par le fait que l'additionneur fonctionnait à une fréquence double de celle du processeur ! Le résultat de ce fonctionnement franchement bizarre, est que les 16 bits de poids faible étaient calculés en une moitié de cycle processeur, alors que l'opération complète prenait un cycle. Deux additions consécutives s'exécutaient donc en 1 cycle et demi, alors qu'on aurait cru au premier abord que cela prendrait seulement un cycle. Si on fait les calculs, on s'apercoit que le rythme de croisière est cependant proche de 2 additions par cycle, bien qu'inférieur. 3 additions consécutives se font en deux cycles, 5 additions en 3 cycles, 7 en 4 cycles, etc. Et le Pentium 4 ajoutait un système de contournement interne à l'ALU. En clair, si une addition utilise le résultat de l'addition précédente, les deux peuvent s'exécuter en un cycle d'horloge et demi. Les 16 bits de poids faible de la première addition sont disponibles après un cycle ALU, ce qui permet de démarrer le calcul de la seconde addition au cycle suivant. ===Le ''replay pipeline''=== Le processeur est un processeur triple émission : il peut charger et décoder 3 µops par cycle, le ROB peut terminer 3 µops par cycle, etc. Pourtant, les ports d'émission peuvent émettre 6 instructions par cycle : 4 ports, dont deux à double fréquence. Une telle différence s'explique par l'usage d'un ''replay pipeline'', dont nous avons déjà parlé dans ce cours. Pour rappel, le Pentium 4 suppose que les lectures font tous un succès de cache L1. Si une opération arithmétique utilise la donnée lue comme opérande, le processeur l'émet immédiatement, l'opérande sera disponible une fois l'opération en entrée de l'ALU entière. Mais s'il s'est trompé, le processeur ré-exécute l'instruction après un temps d'attente de quelques cycles, pour se caler sur la latence du cache L2. Et si il y a un défaut de cache L2, l’instruction attend encore. Cela demande de ré-exécuter des instructions émises à tord, ce qui fait que le processeur doit avoir la capacité d'exécution pour. Ce pourquoi le processeur peut émettre 6 µops dans les unités de calcul : 3 µops normales et 3 µops ré-exécutées. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Les processeurs superscalaires | prevText=Les processeurs superscalaires | next=Les processeurs VLIW et EPIC | nextText=Les processeurs VLIW et EPIC }} </noinclude> ao89p7n0s0834tvf77e6vqryelkfc6h 772751 772750 2026-09-21T21:09:43Z Mewtow 31375 /* Les microarchitectures récentes d'Intel */ 772751 wikitext text/x-wiki Dans ce chapitre, nous allons étudier des exemples de processeurs x86, ceux présents dans nos PC. Nous n'allons pas voir les anciens processeurs comme le 286, le 386 ou le 486. Nous allons commencer avec le Pentium 1, et les processeurs commerciaux qui ont suivis. Tous les processeurs que nous allons voir dans ce chapitre sont des processeurs superscalaires. De fait, ce n'est pas pour rien si ce chapitre se situe après le chapitre sur les processeurs superscalaires. Par contre, nous allons voir que certains n'ont pas d'exécution dans le désordre. La raison est que l'exécution dans le désordre est arrivé après la superscalarité. ==Généralités sur les CPU x86 superscalaires== Avant de voir chaque processeur indépendamment des autres, nous allons devoir aborder quelques généralités. Nous allons d'abord voir que le jeu d'instruction x86 pose quelques problèmes pour la superscalarité. Et ces problèmes posent des contraintes assez fortes, avec lesquelles les concepteurs de processeurs dovient faire avec. Nous poursuivrons ensuite par un historique des processeurs Intel et AMD, histoire de donner un peu de contexte aux processeurs que nous allons étudier. ===Le jeu d'instruction x86 pose des problèmes pour la superscalarité=== Une difficulté de l'architecture x86 est qu'il s'agit d'une architecture CISC, avec tous les défauts que ça implique. Un jeu d'instruction CISC a en effet de nombreuses propriétés qui collent mal avec l'émission multiple, avec la '''superscalarité'''. Il y en a plusieurs, certaines impactent le chargement des instructions, d'autres leur décodage, d'autres l'exécution, etc. Premièrement, les instructions sont de longueur variable, entre 1 et 15 octets, ce qui complique leur chargement et leur décodage. En pratique, les processeurs chargent un bloc de 32 à 64 octets, et découpent celui-ci en plusieurs instructions. La conséquence est que l'usage d'instructions trop longues peut poser problème. Imaginez qu'un processeur charge un bloc de 16 octets et que celui-ci ne contienne qu'une seule instruction : on ne profite pas de la superscalarité. Deuxièmement, une partie des instructions est microcodée, faute de mieux. Et cela pose de sérieux challenges pour l'implémentation des décodeurs. Dupliquer le microcode demanderait trop de transistors, ce qui fait que ce n'est pas fait. À la place, il n'y a qu'un seul microcode, ce qui fait que l'on ne peut pas décoder plusieurs instructions microcodées en même temps. Il est cependant possible de profiter de la superscalarité, en décodant une instruction microcodée en parallèle d'autres instructions non-microcodées. Et heureusement, ce cas est de loin le plus fréquent, il est rare que plusieurs instructions microcodées se suivent. Troisièmement, la présence d'instructions ''load-up'', qui lisent un opérande en mémoire, peut poser problème, mais est aussi source d'optimisations assez intéressantes. En théorie, une instruction ''load-op'' est décodée en deux micro-opération : une pour lire d'opérande en RAM, l'autre pour faire l'opération arithmétique. Sauf que les processeurs x86 modernes optimisent la gestion des instructions ''load-up''. Par exemple, les premiers processeurs Atom géraient des micro-opérations de type ''load-up'', directement dans le chemin de données ! D'autres processeurs utilisent la technique de la '''micro-fusion''' pour retarder le décodage réel des instructions ''load-up'' assez loin dans le pipeline. Avec eux, une instruction ''load-op'' est décodée en une seule "macro-opération", qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. L'avantage est qu'une macro-opération ne prend qu'une seule entrée dans le tampon de ré-ordonnancement, la fenêtre d'instruction, la file de micro-opération, et les autres structures similaires. ===Un petit historique des processeurs x86 superscalaires=== Nous allons voir les processeurs Intel à part des processeurs AMD. La raison à cela est que les architectures Intel et AMD ont progressivement évolué, chacune se basant sur la précédente et l'améliorant. Il n'y a pas eu de cassure entre microarchitectures AMD, qui sont chacune la suite de la précédente. Il est donc préférable de voir les architectures AMD dans l'ordre chronologique. Par contre, Intel a eu une gigantesque cassure, avec le processeur Pentium 4. Son architecture se démarquait fortement du Pentium 3, mais elle n'a pas convaincu et a été abandonnée avec les processeurs suivants. Ce qui fait nous verrons l'architecture du Pentium 4 à part. Le Pentium 1 et 2 utilisaient la même architecture, qu'on détaillera dans ce qui suit. Les seules différences importantes étaient la fréquence, le cache, et quelques détails dans le genre. Le Pentium 3 était une nouvelle microarchitecture qui ajoutait l'exécution dans le désordre. Un an et demi plus tard, le Pentium 4 est sorti et a été un échec. Ses performances étaient peu convaincantes face au Pentium 3, et sa consommation énergétique était très importante. La conséquence est que le Pentium 4 et le Pentium 3 ont survécu pendant un long moment, beaucoup de monde préférait acheter un Pentium 3. Intel a alors amélioré les microarchitectures du Pentium 3 et du 4, indépendamment, pendant environ 7 ans. La microarchitecture du Pentium 3 a subit plusieurs micro-évolutions, chacune avec une finesse de gravure différente, afin de satisfaire les consomateurs. Les premeirs Pentium 3 avaient une finesse de gravure de 250 nm, elle a chuté à 65 sur les derniers modèles. L'architecture du Pentium 4 a fait la même chose, pour tenter de corriger ses problèmes de performance et de consommation d'énergie. Les premiers Pentium 4 avaient uen finesse de gravure de 180 nm, elle a elle aussi chutée à 65 sur les derniers modèles. Après l'échec du Pentium 4, les ingénieurs d'Intel ont repris l'architecture P6 et l'ont améliorée fortement, pour donner l'architecture Core. Les micro-processeurs suivants ont fait évoluer cette architecture progressivement, au point où elle ne ressemble plus à l'originale. L'architecture Core a laissé la place à l'architecture Nehalem, puis Sandy Bridge, puis Haswell, puis Skylake, puis Ice Lake, et Golden Cove. Il s'agit de la lignée principale, partant du Pentium 3 et continuant jusqu'à nos jours. Ces microarchitectures ont suivi un motif assez simple, appelé modèle '''tick-tock'''. Chaque microarchitecture était déclinée en deux versions, la seconde ayant une finesse de gravure réduite. En parallèle, Intel a travaillé sur des processeurs basse performance et basse consommation, avec une microarchitecture très différente. Les processeurs Atom de microarchitecture Bonnel, pour être ensuite remplacés par les microarchitectures Silvermont, puis Goldmont et Gracemont. Ces microarchitectures ont évolué en parallèle de la lignée principale, il s'agit d'une lignée secondaire. Le tout est résumé dans ce schéma ci-dessous. [[File:IntelProcessorRoadmap-4v.svg|centre|vignette|upright=2.5|Roadmap des processeurs Intel, qui servira de structure pour la suite du chapitre]] De nos jours, les processeurs Intel utilisent les deux types de micro-architectures en même temps. Les CPU Intel modernes disposent de deux types de coeurs : les coeurs P et le coeurs E. Leur nom signifie "Performance" et "Efficient", qui trahissent leur but. Les coeurs P utilisent la microarchitecture de la lignée principale, qui est conçue pour la performance. Les coeurs E, quant à eux, utilisent les micro-architectures basse consommation de la lignée secondaire. ==Les processeurs x86 d'Intel, la lignée principale== Pour commencer, nous allons voir les processeurs Intel. N'y voyez pas du favoritisme derrière ce choix, la justification est toute autre. Si je commence par Intel, c'est pour commencer avec les Pentium 1 et 2, qui sont plus simples que tous les autres. Il s'agit en effet de processeurs superscalaires, mais sans exécution dans le désordre. L’absence d'exécution dans le désordre les rend bien plus simples à étudier que les autres, ce qui en fait un bon point de départ pour ce chapitre. Le successeur du Pentium 2 a intégré l'exécution dans le désordre, ce qui fait que les Pentium 1 et 2 sont les seuls processeurs superscalaires ''in-order''. Pour la concurrence, AMD n'a pas produit de processeurs ''in-order'', tous les processeurs produits par AMD intègrent l'exécution dans le désordre. A la rigueur, il y a bien les processeurs Atom de la lignée secondaire d'Intel. Cependant, même s'ils sont bien des CPU ''in-order'', leur architecture est assez compliquée. En comparaison, le Pentium est une vieille architecture, qui se débrouillait avec peu de transistors et était donc bien plus simple que celle de l'Atom. ===Le Pentium 1/MMX et les pipelines U/V=== Le processeur Pentium d'Intel avait un pipeline de 5 étages : un étage de chargement/prédiction de branchement, deux étages de décodage, un étage d'exécution et un dernier étage pour l'écriture dans les registres. Le Pentium 1 était un processeur double émission, intégrant deux pipelines nommés U et V. Chose importante, les deux pipelines n'étaient pas identiques. Le pipeline U pouvait exécuter toutes les instructions, mais le pipeline V était beaucoup plus limité. Par exemple, seul le pipeline U peut faire des calculs flottants, le pipeline V ne fait que des calculs entiers et des branchements. Les deux pipelines disposaient d'une unité de calcul entière, identique dans les deux pipelines. Mais le pipeline U incorporait un circuit multiplieur/diviseur et d'un ''barrel shifter''. L'unité flottante était sur le port d'émission du pipeline U, idem pour l'unité de calcul vectoriel MMX sur le Pentium MMX. Les deux pipelines avaient chacun une unité de calcul d'adresse, mais ils n'étaient pas identiques : celle du pipeline V ne gérait que l’instruction LEA, celle du pipeline U gérait tous les calculs d'adresse. {|class="wikitable" |- ! Pipeline U ! Pipeline V |- | ALU entière | ALU entière |- | Multiplieur/diviseur | |- | ''Barrel Shifter'' | |- | AGU complexe | AGU simple (opération LEA) |- | FPU | |- | Unité SIMD | |} Les deux pipelines géraient les opérations bit à bit, les additions, les soustractions et les comparaisons. Les autres instructions ne sont exécutables que dans le pipeline U. Pour être plus précis, les deux pipelines supportaient les instructions suivantes, ce qui fait qu'on pouvait en faire deux en même temps : * Les instructions arithmétiques INC, DEC, ADD, SUB ; * l'instruction de comparaison CMP ; * les instructions bit à bit AND, OR, XOR ; * l'instruction de calcul d'adresse LEA ; * l'instruction MOV (dépend du mode d'adressage) ; * les instructions de gestion de la pile PUSH et POP (dépend du mode d'adressage) ; * l'instruction NOP, qui ne fait rien. Il faut noter qu'il y a cependant quelques restrictions, beaucoup de paires d'instructions sont interdites. La plupart interdisent au pipeline V de faire quoique ce soit quand une opération particulière est émise dans le pipeline U. Par exemple, si le pipeline U exécute une multiplication ou une division, le processeur ne peut pas exécuter une opération dans le pipeline V. Et c'est pareil avec les branchements : si un branchement est émis dans le pipeline U, l'instruction suivant le branchement n'est pas émise dans le pipeline V, pour éliminer les dépendances de contrôle. De même, si le pipeline U exécute une opération flottante, le pipeline V ne pourra rien exécuter. La seule exception est l'instruction FCXH, qui échange deux registres flottants. [[File:Intel Pentium arch.svg|centre|vignette|upright=2.5|Microarchitecture de l'Intel Pentium MMX. On voit que certaines unités de calcul sont dupliquées.]] Un choix assez intéressant a été fait pour le cache de données. Nous avions vu dans le chapitre sur les CPU superscalaires que la superscalarité a un impact sur l'unité mémoire. Il y a alors deux implémentations. La première ne fait rien, l'unité mémoire ne change pas, et le processeur ne peut pas faire deux accès mémoire simultanés. La seconde duplique l'unité mémoire et les ports de lecture/écriture du cache, ce qui autorise des accès mémoire simultanés. Le Pentium 1 utilise une solution intermédiaire. Les ingénieurs d'Intel étaient partis à la base sur un cache totalement double port, pour obtenir des performances maximales. Mais diverses simulations et observations les ont fait changer d'avis. Les simulations ont montré qu'il est "rare" que les deux pipelines aient besoin de lire/écrire dans le cache en même temps. Et ils ont optimisé le cache de donnée pour en tenir compte. Le cache de données est partiellement multiport : simple port sur certains aspects, double port sur d'autres. Le cache est un cache splité, à savoir que les données et les ''tags'' sont séparés dans des mémoires séparées. La mémoire pour les ''tags'' est multiport, ce qui permet d'interroger les ''tags'' du cache deux fois par cycle. Un port est relié au pipeline u, un autre au pipeline V. Mais pour les données, le cache n'a qu'un seul port pour lire/écrire des données. Impossible donc de lire deux données en même temps, pour alimenter les deux pipelines. Il utilise cependant 8 banques permet d'accélérer des accès mémoire proches dans le temps, mais dans des cycles d'horloge différents. Entre les deux mémoires, il y a un circuit qui détecte les conflits, à savoir les situations où les deux pipelines accèdent en même temps au cache. S'ils veulent lire/écrire une donnée dans le même cycle, ce qui est impossible avec un seul port, le pipeline U a la priorité et le pipeline V attend le cycle suivant. Le circuit détecte aussi les conflits de banque, à savoir quand un pipeline accède à une banque en cours d'accès par l'autre pipeline (on rappelle qu'un accès au cache prend plusieurs cycles). Le circuit détecte aussi certaines dépendances mémoires, à savoir des accès consécutifs à la même adresse. : La TLB du processeur est aussi totalement double port. ===La microarchitecture P6 du Pentium 2/3=== Le Pentium 3 utilisait la '''microarchitecture P6''', qui a été dérivée dans de nombreuses variantes, dont les finesses de gravure n'étaient pas les mêmes. Il introduit une exécution dans le désordre simple, avec une fenêtre d'instruction centralisée, avec renommage dans le désordre dans le ROB (tampon de ré-ordonnancement), commandé par une table d'alias. C'était un processeur triple émission, soit une instruction de plus que la double émission du Pentium 1. Le pipeline passe de 5 étage sur le Pentium à 14 - 12 étages, dont le détail est le suivant : * Prédiction de branchement, deux cycles ; * Chargement des instructions, trois cycles ; * Décodage de l'instruction, deux cycles ; * Renommage de registre, un cycle ; * Copie des opérandes dans le tampon de ré-ordonnancement (lié au renommage de registre dans le ROB) ; * Dispath dans ou depuis la station de réservation. * Exécution de l'instruction ; * Écriture du résultat dans le ROB ; * Écriture dans le banc de registre physique. Les instructions sont chargées par blocs de 16 octets, avec un système de fusion de blocs pour gérer les instructions à cheval sur deux blocs. Lors d'un branchement, deux blocs doivent être chargés si l'instruction de destination n'est pas alignée sur 16 octets et cela cause un délai de un cycle d'horloge. Le décodage des instructions x86 était géré par plusieurs décodeurs. Il y avait trois décodeurs : deux décodeurs simples, et un décodeur complexe. Les décodeurs simples décodaient les instructions les plus fréquentes, mais aussi les plus simples, qui étaient décodées en une seule micro-opération. Les instructions CISC complexes étaient gérées uniquement par le décodeur complexe, basé sur un microcode, qui pouvait fournir jusqu'à 4 micro-opérations par cycle. Le tout est résumé avec la règle 4-1-1. La toute première instruction chargée depuis la file d'instruction va dans le premier décodeur simple. Si jamais le décodeur ne peut pas décoder l'instruction, l'instruction est redirigée dans un autre décodeur, avec un délai d'un cycle d'horloge. Les stations de réservations étaient regroupées dans une structure centralisée, en sortie de l'unité de renommage. Elles avaient 5 ports d'émission, qui étaient sous-utilisés en pratique. Niveau ALU, on trouve deux ALUs entières, une flottante, une unité pour les instructions SSE et autres, et trois unités pour les accès mémoire (regroupées en une seule unité dans le schéma ci-dessous). Les unités mémoire regroupent une unité de calcul d'adresse pour les lectures, une autre pour les écritures, et une unité pour la gestion des données à écrire. Les unités de calcul d'adresse sont des additionneurs à 4 opérandes, complétement différents des ALU entières. Les ALU entières sont deux unités asymétriques : une ALU simple, et une ALU complexe incorporant un multiplieur. Les deux peuvent exécuter des opérations d'addition, soustraction, comparaison, etc. [[File:P6 func diag.png|centre|vignette|upright=2|P6 func diag]] Les premiers Pentium 3 n'avaient pas de cache L2 dans le processeur, celui-ci était sur la carte mère. Mais il a été intégré dans le processeur sur la seconde version du Pentium 3, la version Coppermine. Le Pentium 3 a servi de base aux microarchitectures d'Intel qui ont suivi. Les changements à chaque nouvelle génération sont assez mineurs : la prédiction de branchement est améliorée, la taille des stations de réservation et du ROB augmente, idem avec les autres structures liées à l'exécution dans le désordre. Les processeurs Intel ont conservé une fenêtre d'instruction centralisée, alors qu'AMD utilise une autre méthode, comme nous allons le voir dans ce qui suit. Les seuls changements notables sont est le passage à un renommage dans le ROB à un renommage à banc de registre physique, ainsi que l'introduction du cache de micro-opération. Et ce sont des modifications qu'AMD a aussi faites, celle-ci étant clairement une bonne idée pour toutes les micro-architectures avec un budget en transistor suffisant. Il est intéressant de garder cela en tête, car une bonne partie des améliorations de chaque micro-architecture proviendra de là. ===La microarchitecture Core=== La '''microarchitecture Core''' fait suite au Pentium 4, mais reprend en fait beaucoup d’éléments du Pentium 2 et 3. Elle utilise la station de réservation unique avec renommage dans le ROB, provenant du Pentium 2/3. Elle supporte aussi les optimisations des opérations ''load-up'', avec notamment un support des macro-opérations mentionnées plus haut. Les améliorations sont assez diverses, mais aussi assez mineures. * Le processeur incorpore un cache L2, en plus des caches L1 déjà présents auparavant. * La prédiction de branchement a été améliorée avec notamment l'ajout d'une ''Fetch Input Queue''. * L'architecture Core passe à la quadruple émission, soit une instruction de plus que sur le Pentium 2 et 3. Pour cela, un quatrième décodeur est ajouté, il s'agit d'un décodeur simple qui ne fournit qu'une seule micro-opération en sortie. * Un ''stack engine'' et un ''Loop Stream Detector'' ont été ajoutés, ainsi que le support de la macro-fusion qui fusionne une instruction de test et le branchement qui suit en une seule micro-opération. * Les techniques de désambiguïsation mémoire sont implémentées sur cette micro-architecture. Il y a quelques modifications au niveau de l'unité de chargement. La file d'instruction a toujours ce système de fusion de blocs, sauf que les branchements ne causent plus de délai d'un cycle lors du chargement. La file d'instruction est suivie par un circuit de prédécodage qui détermine la taille des instructions et leurs frontières, avant de mémoriser le tout dans une file de 40 instructions. La station de réservation dispose de 6 ports d'émission, mais on devrait plutôt dire 5. Sur les 5, il y en a un pour les lectures, un pour les écritures. Les deux sont reliées à une ''Load/Store Queue'', appelée ''Memory Ordering Buffer''. Elle est elle-même reliée au cache de données par deux ports : un port de lecture et un port d'écriture. Les trois ports d'émission restants sont connectés aux unités de calcul. Les trois ports récents sont chacun reliés à une ALU entière. L'additionneur flottant est connecté au port 1, alors que le multiplieur/diviseur flottante est connecté au port 0. Le fait de mettre les deux sur des ports différents permet d'émettre une addition et une multiplication flottant simultanément. Le multiplieur entier est relié au second port d'émission, celui sur lequel se trouve l'additionneur flottant. Le résultat que le processeur peut émettre un mix d'opérations flottantes et entière assez varié. [[Image:Intel Core2 arch.svg|centre|vignette|upright=2|Intel Core microarchitecture]] ===Les microarchitectures Sandy Bridge and Ivy Bridge=== Les micro-architectures suivant la micro-architecture Core ont introduit quelques grandes modifications : le passage à un renommage à banc de registre physique, l'ajout d'un cache de micro-opérations (et d'un ''Loop Stream Detector''). L'ajout du cache de micro-opérations est un gros changement, particulièrement avec le jeu d’instruction x86. Le décodage des instructions est lent, couteux en énergie. Mais avec l'introduction du cache de micro-opération, la majorité des micro-opérations est non pas décodée, mais lue depuis le cache de micro-opérations. Les décodeurs décodent les instructions pas encore exécutées, mais les exécutions suivantes sont lues depuis le cache de micro-opérations. Et vu la grande présence de boucles, le cache de micro-opérations est l'alimentation principale du pipeline. Les décodeurs servent surtout à alimenter le cache de micro-opérations, parfois décoder quelques instructions isolées exécutées de-dehors de boucles, pas plus. Concrètement, ils servent pour 10 à 20% des micro-opérations exécutées. Intel a d'ailleurs reflété ce fait dans sa terminologie. Intel distingue deux voies de chargement : le ''legacy pipeline'' et le cache de micro-opérations. L'unité de chargement et les décodeurs sont regroupés dans la voie du ''legacy pipeline''. Le cache de micro-opérations est complété avec un ''Loop Stream Detector'', placé après le cache en question. Les décodeurs et le cache de micro-opérations alimentent une file de micro-opérations, située juste avant l'étage de renommage de registres. La file de micro-opérations sert en quelque sorte de tampon entre l'étage de "décodage" et celui de renommage. Le ''Loop Stream Detector'' utilise cette file de micro-opérations comme d'un cache lorsqu'une boucle est détectée. Les micro-opérations de la boucle sont lue depuis la file de micro-opérations, pour être envoyée au renommeur de registres. L'avantage est que le cache de micro-opérations et/ou les décodeurs sont mis en pause et clock-gatés lorsqu'une boucle s'exécute, ce qui réduit la consommation du processeur. Le ''Loop Stream Detector'' et le cache de micro-opération ont globalement le même effet : désactiver tout ce qui est avant, le ''Loop Stream Detector'' appliquant cette méthode au cache de micro-opération lui-même.. Voyons maintenant quelles sont les micro-architectures qui implémentent ces optimisations. Les microarchitectures '''Sandy Bridge''' and '''Ivy Bridge''' sont similaires à l'architecture Core, si ce n'est pour le passage à un renommage à banc de registre physique, et l'ajout d'un cache de micro-opérations. Le nombre de ports d'émission passe à 7, avec 4 pour les instructions arithmétiques (flottantes comme entière), 2 pour les lectures, et un pour les écritures (en fait deux, avec un pour le calcul d'adresse, l'autre pour la donnée à écrire). Pour le reste, rien ne change si ce n'est la prédiction de branchement Les architectures '''Haswell''' et '''Broadwell''' ont ajouté quelques unités de calcul, élargit la sortie du cache de micro-opérations. Un port d'émission pour opération entières a été ajouté, de même qu'un port pour les accès mémoire. Le processeur passe donc à 8 ports d'émission, ce qui permet d'émettre jusqu'à 8 micro-opérations, à condition que le cache de micro-opération suive. Pour le reste, le processeur est similaire aux architectures précédentes, si ce n'est que certaines structures grossissent. L'architecture '''Skylake''' réorganise les unités de calcul et les ports d'émission pour gagner en efficacité. Pour le reste, les améliorations sont mineures. A la rigueur, l'unité de renommage de registre ajoute des optimisations comme l'élimination des MOV, les idiomes liés aux opérations avec zéro, etc. ===Les microarchitectures récentes d'Intel=== Les architectures '''Ice Lake''' et '''Tiger Lake''' passent de quadruple émission à la pentuple émission. Par contre, le processeur utilise toujours 4 décodeurs. Mais les micro-opérations étant émises depuis le cache de micro-opérations, ce n'est pas un problème pour la pentuple émission. Le processeur peut parfaitement émettre 5 micro-opérations en même temps, si elles sont lues depuis le cache de micro-opérations. Là encore, on voit à quel point le cache de micro-opération découple ce qu'il y avant de ce qu'il y a après. La microarchitecture '''Golden Cove''' altère les décodeurs et l'unité de chargement. Sur toutes les générations précédentes, on reste sur une unité de chargement qui charge 16 octets à la fois et il y a toujours 4 décodeurs identiques aux générations précédentes. Golden Cove passe à 6 décodeurs simples, et double la taille du chargement qui passe à 32 octets. Une telle stagnation sur les unités de chargement et de décodage s'explique encore une fois par la présence du cache de micro-opération fait que ce n'est pas trop un problème. Tout ce qui précède le cache de micro-opérations n'a pas de raison d'évoluer, car ce cache est très puissant. [[File:Golden Cove.png|centre|vignette|upright=3|Golden Cove]] ==Un étude des microarchitectures superscalaires x86 d'AMD== Les architectures Intel ont évolué progressivement, sans grandes cassure. Il y a une continuité presque initerrompue entre l'architecture du Pentium 2 et les architectures modernes. Intel a fait des améliorations mineures à chaque nouvelle micro-architecture, si on omet le passage à un renommage à banc de registre physique et l'ajout du cache de micro-opération. A l'opposé, les architectures AMD ont eu de nombreuses cassures dans la continuité où AMD a revu sa copie de fond en comble. Étudier ces architectures demande de voir trois choses séparément : le ''front-end'' qui regroupe l'unité de chargement et les décodeurs, le ''back-end'' qui gère l'exécution dans le désordre et les unités de calcul, et le sous-système mémoire avec les caches et la ''Load Store Queue''. Leur étude sera plus ou moins séparée dans ce qui suit, pour chaque classe d'architecture. ===La première génération de CPU AMD : les architectures K5, K6, K7, K8 et K10=== La première génération de processeurs AMD est celle des architectures K5, K6, K7, K8 et K10. Il n'y a pas de K9, qui a été abandonné en cours de développement. Les processeurs K5 et K6 portent ce nom au niveau commercial. Par contre, les processeurs d'architecture K7 sont aussi connus sous le nom d''''AMD Athlon''', les AMD K8 sont connus sous le nom d''''AMD Athlon 64''', et les architecture K10 sont appelées les '''AMD Phenom'''. Comme le nom l'indique, l'architecture K8 a introduit le 64 bits chez les processeurs AMD. Elles ont une architecture assez similaire pour ce qui est du chargement et des caches. Toutes disposent d'au minimum un cache L1 d'instruction et d'un cache L1 de données. Le K5 n'avait que ces caches, mais un cache L2 a été ajouté avec le K7, puis un L3 avec le K10. L'AMD K5 avait une TLB unique, mais les processeurs suivants avaient une TLB pour le L1 d'instruction et une autre pour le L1 de données. Idem pour le cache L2, avec deux TLB : une pour les données, une pour les instructions. Les caches L1/L2 sont de type exclusifs, à savoir que les données dans le L1 ne sont pas recopiées dans le L2. Le cache L2 est précisément un cache de victime, qui mémorise les données/instructions, évincées des caches L1 lors du remplacement des lignes de cache. L'introduction du cache L2 a entrainé l'ajout de deux TLB de second niveau : une L2 TLB pour les données et une autre pour les instructions. Les architectures K8 et K10 ont ajouté un cache L3, avec un accès indirect à travers l'interface avec le bus. : L'AMD K7 originel, aussi appelée Athlon classique, n'avait pas de cache L2, mais celui-ci était placé sur la carte mère et fonctionnait à une fréquence moitié moindre de celle du CPU. L'Athlon Thunderbird, puis l'Athlon XP, ont intégré le cache L2 dans le processeur. {|class="wikitable" |- ! Architecture AMD ! colspan="5" | Caches |- | rowspan="2" | K5 | L1 instruction || L1 données || colspan="3" | |- | colspan="2" | TLB unique || colspan="3" | |- | colspan="4" | |- | rowspan="2" | K6 | L1 instruction || L1 données || colspan="3" | L2 unifié |- | TLB L1 instruction || TLB L1 données || colspan="3" | |- | colspan="6" | |- | rowspan="2" | K7, K8 | L1 instruction || L1 données || colspan="2" | L2 unifié || |- | TLB L1 instruction || TLB L1 données || TLB L2 instruction || TLB L2 données || |- | colspan="6" | |- | rowspan="2" | K10 | L1 instruction || L1 données || colspan="2" | L2 unifié || L3 |- | TLB L1 instruction || TLB L1 données || TLB L2 instruction || TLB L2 données || |} Fait important, les architectures K5 à K10 utilisent la technique du '''prédécodage''', où les instructions sont partiellement décodées avant d'entrer dans le cache d'instruction. Le prédécodage facilite grandement le travail des décodeurs d'instruction proprement dit. Par contre, le prédécodage prend de la place dans le cache L1 d'instruction, une partie de sa capacité est utilisé pour mémoriser les informations prédécodées. C'est donc un compromis entre taille du cache et taille/rapidité des décodeurs d'instruction. Sur les architectures K5 et K6, le prédécodage précise, pour chaque octet, si c'est le début ou la fin d'une instruction, si c'est un octet d'opcode, en combien de micro-opérations sera décodée l'instruction, etc. A partir de l'AMD K7, le prédécodage reconnait les branchements inconditionnels. Lorsqu'un branchement inconditionnel est pré-décodé, le pré-décodage tient compte du branchement et continue le pré-décodage des instructions à partir de la destination du branchement. Le système de prédécodage est abandonnée à partir de l'architecture Bulldozer, qui suit l'architecture K10. La prédiction de branchement de ces CPU tire partie de ce système de pré-décodage, à savoir que les prédictions de branchement sont partiellement mémorisées dans les lignes de cache du L1 d'instruction. Par exemple, l'AMD K5 se passe de ''Branch Target Buffer'' grâce à cela. Si une ligne de cache contient un branchement, elle mémorise l'adresse de destination de ce branchement, en plus des bits de pré-décodage. Si il y a plusieurs branchements dans une ligne de cache, c'est l'adresse de destination du premier branchement pris dans cette ligne de cache qui est mémoirsée. Un défaut de cette approche est que si le branchement n'est pas dans le L1 d'instruction, aucune prédiction de branchement ne peut être faite et le préchargement ne peut pas fonctionner. C'est une limitation que n'ont pas les BTB découplées du cache L1 : elles peuvent prédire un branchement qui a été évincé dans le L2 ou le L3, tant que l'entrée associée est dans le BTB. Les prédictions peuvent même servir à précharger les instructions utiles. [[File:Comparaison du chargement de l'AMD K5 et K6.png|centre|vignette|upright=2|Comparaison du chargement de l'AMD K5 et K6]] Au niveau du décodage, on trouve de nombreuses différences entre les premières architectures AMD. L'AMD K5 contient 4 décodeurs hybrides, afin de décoder 4 instructions par cycles. Le K5 a quatre décodeurs simples couplés à 4 décodeurs complexes avec chacun un accès au micro-code. Une instruction peut donc passer par a donc deux voies de décodage : un décodage rapide et simple pour les instructions simples, un décodage lent et passant par le microcode pour les instructions complexes. Pour décoder 4 instructions, les deux voies sont dupliquées en 4 exemplaires, ce qui a un cout en circuits non-négligeable. L'AMD K6 utilise moins de décodeurs et ne peut que décoder deux instructions à la fois maximum. Par contre, il fournit en sortie 4 micro-opérations. Il intègre pour cela deux décodeurs simples, un décodeur complexe et un décodeur micro-codé. Un décodeur simple transforme une instruction simple en une ou deux micro-opérations. Il est possible d'utiliser les deux décodeurs simples en même temps, afin de fournir 4 micro-opérations en sortie du décodeur. Les deux autres décodent une instruction complexe en 1 à 4 micro-opérations. Si jamais la ou les deux instructions sont décodées en 1, 2 ou 3 micro-opérations, les micro-opérations manquantes pour atteindre 4 sont remplies par des NOPs. Pour le K7 et au-delà, le processeur dispose de décodeurs séparées pour les instructions micro-codées de celles qui ne le sont pas. Le processeur peut décoder jusqu’à 3 instructions par cycle. Le décodage d'une instruction microcodée ne peut pas se faire en parallèle du décodage non-microcodé. C'est soit le décodeur microcodé qui est utilisé, soit les décodeurs câblés, pas les deux en même temps. Le décodage d'une instruction prend 4 cycles. Les instructions non-microcodées sont décodées en une seule micro-opération, à un détail près : le CPU optimise la prise en charge des instructions ''load-up''. La différence entre le K6 et le K7 s'explique par des optimisations des instructions ''load-up''. Sur le K6, les instructions ''load-up'' sont décodées en deux micro-opération : la lecture en RAM, l'opération proprement dite. Mais sur le K7, une instruction ''load-up'' est décodée en une seule micro-opération. En conséquence, les décodeurs simples sont fortement simplifiés et le décodeur complexe disparait au profit d'un microcode unique. [[File:Décodage sur le K5 et le K5.png|centre|vignette|upright=3|Décodage sur le K5 et le K5]] ====Les microarchitectures K5 et K6 d'AMD==== Les deux premières architectures étaient les architectures K5 et K6, l'architecture K6 ayant été déclinée en quatre versions, nommées K6-1, K6-2, et K-3, avec une version K6-3 bis. Elles sont regroupées ensemble car elles ont beaucoup de points communs. Par exemple, tout ce qui a trait au chargement et au cache était similaire, de même que les unités de calcul. Les deux architectures avaient n'avaient pas de cache L2 et devaient se contenter d'un cache L1 d'instruction et d'un cache L1 de données. L'AMD K5 incorpore une TLB unique, alors que le K6 utilise des TLB séparées pour le cache d'instruction et le cache de données. Une différence entre l'architecture K5 et K6 est que la première utilise des caches normaux, alors que la seconde utilise des ''sector caches''. Les deux architectures disposaient des unités de calcul suivantes : deux ALU entières, une FPU, deux unités LOAD/STORE pour les accès mémoire, une unité de branchement et une ou plusieurs unités SIMD. Une organisation classique, donc. Pour les unités entières, il y avait deux ALU simples, un ''barrel shifter'' et un diviseur. Il n'y a pas d'erreur, le processeur incorpore un circuit diviseur, mais pas de circuit multiplieur. La raison est que la multiplication est réalisée par la FPU ! En effet, le multiplieur flottant de la FPU intègre un multiplieur entier pour multiplier les mantisses, qui est utilisé pour les multiplications entières. La même technique a été utilisée sur l'Atom, comme vu plus haut. Le tout était alimenté par deux ports d'émission, appelés ports X et Y. Sur l'architecture K5, le ''barrel shifter'' et le diviseur sont des ports différents. {|class="wikitable" |+ AMD K5 |- ! Port X ! Port Y |- | ALU simple | ALU simple |- | ''Barrel Shifter'' | Diviseur |} Sur l'architecture K6, le ''barrel shifter'' et le diviseur sont sur le même port. {|class="wikitable" |+ AMD K6 |- ! Port X ! Port Y |- | ALU simple | ALU simple |- | | ''Barrel Shifter'' |- | | Diviseur |} Niveau unités mémoire, le K5 avait deux unités LOAD/STORE, chacune capable de faire lecture et écriture. Par contre, la ''store queue'' n'a qu'un seul port d'entrée, ce qui fait que le processeur peut seulement accepter une écriture par cycle. Le processeur peut donc émettre soit deux lectures simultanées, soit une lecture accompagnée d'une écriture. Impossible d'émettre deux écritures simultanées, ce qui est de toute façon très rare. L'architecture K6 utilise quant à elle une unité LOAD pour les lectures et une unité STORE pour les écritures. Ce qui permet de faire une lecture et une écriture par cycle, pas autre chose. Niveau unités SIMD, l'architecture K7 n'avait qu'une seule unité SIMD, placée sur le port d'émission X. L'architecture K8 ajouta une seconde unité SIMD, sur l'autre port d'émission entier. De plus, trois ALU SIMD ont été ajoutées : un décaleur MMX, une unité 3DNow!, une unité mixte MMX/3DNow. Elles sont reliées aux deux ports d'émission entier X et Y ! Elles ne sont pas représentées ci-dessous, par souci de simplicité. [[File:Unité de calcul des processeurs AMD K5 et K6.png|centre|vignette|upright=2|Unité de calcul des processeurs AMD K5 et K6. les unités sur la même colonnes sont reliées au même port d'émission.]] Si les unités de calcul et le chargement sont globalement les mêmes, les deux architectures se différencient sur l'exécution dans le désordre. L'AMD K5 utilise du renommage de registre dans le ROB avec des stations de réservation. Par contre, l'AMD K6 utilise une fenêtre d'instruction centralisée. De plus, son renommage de registre se fait avec un banc de registre physique. L'architecture AMD K5 utilisait de deux stations de réservation par unité de calcul, sauf pour les deux unités mémoire partageaient une station de réservation unique (deux fois plus grande). Les stations de réservation sont cependant mal nommées, vu que ce sont en réalité des mémoire FIFO. Une micro-opération n'est émise que si elle est la plus ancienne dans la FIFO/station de réservation. Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique. Le tampon de ré-ordonnancement faisait seulement 16 instructions. [[File:AMD K5.jpg|centre|vignette|upright=3|AMDK5 Diagramme.]] L'architecture K6 remplace les stations de réservations par une fenêtre d'instruction centralisée. Les 4 micro-opérations renommées sont écrites dans la fenêtre d'instruction par groupe de 4, NOP de ''padding'' inclus. La fenêtre d'instruction centralisé contient 24 micro-opérations, groupées en 6 groupes de 4 micro-opérations, avec potentiellement des NOP dedans suivant le résultat du décodage. L'avantage est que l'implémentation de la fenêtre d'instruction est simple. La fenêtre d'instruction centralisée permettait d'émettre 6 micro-opérations en même temps (une par unité de calcul/mémoire). Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique. Le processeur utilisait un renommage avec un banc de registre physique. Le banc de registre physique pour les entiers contenait 48 registres, dont 24 étaient des registres architecturaux et 24 étaient des registres renommés. Sur les 24 registres architecturaux, 16 avaient une fonction de ''scratchpad'' que les ''datasheets'' d'AMD ne détaillent pas, les 8 restants étaient les registres généraux EAX, EBX, etc. [[File:AMD K6 Little foot & Modl 6.png|centre|vignette|upright=3|AMD K6 original.]] ====Les microarchitectures K7, K8 et K10 d'AMD==== Les microarchitectures suivantes sont les architectures K7, K8 et K10. Les architectures K7, K8 et K10 sont assez similaires. La différence principale entre le K7 et le K8 est le support du 64 bits. Les apports du K10 sont la présence d'un cache L3, d'une unité de calcul supplémentaire et d'améliorations de la prédiction de branchement. La taille de certains caches a été augmentée, de même que la largeur de certaines interconnexions/bus. L'architecture K7 des processeurs Athlon utilisait le renommage de registre, mais seulement pour les registres flottants, pas pour les registres entiers. Le ranommeg des registres flottants étaient réalisé via un banc de registres physique, ne contenant que des registres flottants. Les architectures K8 et K10 utilisent le renommage de registres pour tous les registres, entiers comme flottants. Par contre, le renommage de registre n'est pas réalisé de la même manière pour les registres entiers et flottants. Les registres entiers sont renommés dans le tampon de ré-ordonnancement, comme c'était le cas sur les architectures Intel avant le Pentium 4. Par contre, les registres flottants sont renommés grâce à un banc de registre physique. Le K8 est donc un processeur au renommage hybride, qui utilise les deux solutions de renommage principales. A partir du K7, le CPU optimise la prise en charge des instructions ''load-up''. Les instructions ''load-op'' sont appelées des macro-opérations dans la terminologie d'AMD, et aussi d'Intel. L'idée est que les instructions ''load-up'' sont décodées en micro-opérations intermédiaires. Elles sont propagées dans le pipeline comme étant une seule micro-opération, jusqu'à l'étage d'émission. Lors de l'émission, les instructions ''load-up'' sont scindées en deux micro-opérations : la lecture de l'opérande, puis l'opération proprement dite. Faire ainsi économise des ressources et optimise le remplissage du tampon de ré-ordonnancement, des fenêtres d'instructions, des stations de réservation, etc. Le tampon de réordonnancement est combiné avec divers circuits en charge de l'exécution dans le désordre, dans ce qui s'appelle l'''instruction control unit''. Il contient de 72 à, 84 instructions, qui sont regroupées en groupes de 3. Là encore, comme pour le K5 et le K6, le tampon de réordonnancement tient compte de la sortie des décodeurs. Les décodeurs fournissent toujours trois micro-opérations par cycle, quitte à remplir les vides par des NOP. Le tampon de réordonnancement reçoit les micro-opérations, NOP inclus, par groupes de 3, et est structuré autour de ces triplets de micro-opération, y compris en interne. Pour ce qui est de l'unité mémoire, elle est précédée par une file de µops mémoire, qui émet les accès mémoire dans l'ordre du programme. Elle est souvent qualifiée de ''Load-Store Queue'', mais ce n'est pas la terminologie que nous utilisons dans ce cours. La file de micro-opération lire/écrire 64 bits par cycle depuis le cache L1, ce qui fait un seul accès au cache par cycle. La file de µops mémoire est appelée la ''Pre-Cache Queue''. Si au vu de son nom, vous avez deviné qu'il y avait une ''Post-Cache Queue''. Elle mémorise les lectures/écritures émises, mais qui ont levé un défaut de cache L1. Elle ne fait pas partie de la file de µops mémoire proprement dite. Les architectures K7, K8 et K10 ont des unités de calcul très similaires. Concrètement, il y a trois ALU entières, trois unités de calcul d'adresse, et une FPU. Le processeur incorpore, aussi un multiplieur entier, relié sur le port d'émission de la première ALU. La FPU regroupe un additionneur flottant, un multiplieur flottant, et une troisième unité LOAD/STORE pour les lectures/écritures pour les nombres flottants. L'architecture K8 ajoute une unité de manipulation de bit, la K10 un diviseur entier. [[File:Unité de calcul des processeurs AMD K7, K8 et K10.png|centre|vignette|upright=2|Unité de calcul des processeurs AMD K7, K8 et K10]] La manière d'alimenter les ALU en micro-opérations varie un petit peu entre les architectures K7, K8 et K10. Il y a cependant quelques constantes entre les trois. La première est qu'il y a une fenêtre d'instruction séparée pour les flottants, de 36 à 42 entrées, avec renommage de registre. La fenêtre d'instruction flottante a trois ports d'émission : un pour l'additionneur flottant, un autre pour le multiplieur, et un troisième pour la troisième unité flottante qui s'occupe du reste. La seconde est que chaque ALU entière est couplée avec une unité de calcul d'adresse. Par contre, la méthode de couplage varie d'un processeur à l'autre. : Les stations de réservation sont nommées des ''schedulers'' dans les schémas qui suivent. La microarchitecture K7 avait deux fenêtres d'instruction : une pour les opérations flottantes, une autre pour les instructions entières et les accès mémoire. La fenêtre d'instruction entière était reliée à 3 ALU entières et à 3 AGU. Elle pouvait émettre trois micro-opérations en même temps : trois micro-opérations entières, trois micro-opérations mémoire. Les AGU étaient reliées à la file de µops mémoire mentionnée plus haut, ce qui permet d'émettre trois µops mémoire par cycle. Par contre, la file de µops mémoire ne pouvait exécuter qu'une lecture de 64 bits ou une écriture de 64 bits. En clair, trois micro-opérations mémoire peuvent être émises par cycle, cela entraine trois calculs d'adresse simultanés, mais les trois lectures/écritures sont mises en attente dans la file de µops mémoire. Elles s'exécutent alors l'une après l'autre. La fenêtre d'instruction entière contenait 5 à 6 groupes de 3 macro-opérations. Vous noterez que j'ai parlé de macro-opérations et pas de micro-opérations, car les instructions ''load-up'' sont considérées comme une seule "micro-opération" dans la fenêtre d'instruction entière. Et cela se marie bien avec une fenêtre d'instruction unique partagée entre pipeline entier et pipeline mémoire. Une macro-opération était scindée en deux micro-opérations : une micro-opération mémoire et une micro-opération entière. Il est donc avantageux de regrouper unités mémoire et unités entières à la même fenêtre d'instruction pour ce faire. [[File:AMD K7.png|centre|vignette|upright=3|AMD K7]] Sur les architectures K8 et K10, la station de réservation unique de 15 micro-opérations est remplacée par trois stations de réservations, de 8 micro-opérations chacune pour le K8, de 10 pour le K10. Chaque station de réservation entière alimente une unité de calcul entière et une unité de calcul d'adresse. l'unité de calcul d'adresse est reliée à la file de µops mémoire, qui n’exécute toujours qu'un seul accès mémoire par cycle. Le multiplieur est relié à la première station de réservation, sur le même port d'émission que l'ALU. [[File:AMD Husky microarchitecture.png|centre|vignette|upright=3|AMD Husky microarchitecture]] La microarchitecture K10 a été déclinée en plusieurs versions, nommées Grayhound, Grayhound+ et Husky, Husky étant une architecture gravée en 32 nm dédiée aux processeurs A-3000. L'architecture Grayhound a plus de cache et un ROB plus grand, la Husky est quand à elle un peu plus différente. Elle n'a pas de cache L3, contrairement aux autres architectures K10, ce qui simplifie fortement son sous-système mémoire. Par contre, les fenêtres d'instructions/stations de réservation et le ROB sont plus grands, pareil pour les files dans l'unité mémoire. Une ALU pour les divisions entières a aussi été ajoutée. Pour résumer, les architectures K7, K8 et K10 séparent les pipelines entiers et flottants : trois pipelines entiers avec chacun son unité de calcul, et un pipeline flottant avec plusieurs unités de calcul. Les raisons à cela sont assez diverses. Disons que dupliquer des ALU entières simples prend peu de transistors, là où les gros circuits comme le multiplieur ou la FPU ne sont pas dupliqués. Et cela a un autre avantage : le renommage, ''dispatch'' et l'émission sont plus simples. Les pipelines entiers ont une exécution dans le désordre peu complexe, grâce au grand nombre d'unités de calcul, ce qui fait que le pipeline entier est de seulement 15 cycles au total (chargement et décodage inclus). A l'opposé, la FPU est alimentée par une exécution dans le désordre très complexe, avec banc de registre physique et beaucoup de ressources, mais au prix d'un pipeline flottant plus long de 3 cycles, soit 18 cycles au total. ===Les microarchitectures ZEN d'AMD=== Viennent ensuite les '''microarchitectures Bulldozer''', avec trois révisions ultérieures nommées Piledriver, Steamroller et Excavator. Mais du fait de l'utilisation de techniques de multithreading matériel que nous n'avons pas encore abordé, nous ne pouvons pas en parler ici. Les microarchitectures suivantes sont les '''architectures ZEN 1/2/3/4/5'''. Elles se ressemblent beaucoup, chacune accumulant les améliorations des précédentes. Mais le cœur de l'architecture reste plus ou moins le même. En passant à la suivante, le nombre de registre virtuel augmente, le ''branch target buffer'' augmente en taille, le ROB et les files d'attente grossissent, les caches de micro-opération aussi, les caches grossissent, etc. Une optimisation intéressante est l'ajout d'un cache de micro-opération, qui améliore grandement les performances du ''front-end'', notamment pour les boucles. La microarchitecture Zen 1 est illustrée ci-dessous. Comme on le voit, les registres flottants ont une unité de renommage séparée de celle pour les entiers, mais les deux utilisent du renommage à banc de registre physique. Il y a par contre une différence au niveau des fenêtres d'instruction, notées ''scheduler'' dans le schéma. Pour ce qui est des unités de calcul flottantes, il y a une fenêtre unifiée qui alimente quatre ALU, grâce à 4 ports d'émission. Mais pour les ALU entières, il y a une fenêtre d'instruction par ALU, avec un seul port d'émission connecté à une seule ALU. La raison de ce choix est que les opérations flottantes ont un nombre de cycle plus élevé, sans compter que les codes flottants mélangent bien additions et multiplication. Une fois décodées, les instructions sont placées dans une première file de micro-opérations om elles attendent, puis sont dispatchées soit dans le pipeline entier, soit dans le pipeline flottant. les micro-opérations entières sont insérées dans une fenêtre d'instruction directement, alors que les micro-opérations flottantes doivent patienter dans une seconde file de micro-opérations. La raison est que les micro-opérations flottantes ayant une grande latence, trop d'instructions flottantes consécutives pourraient bloquer le pipeline flottant, sa fenêtre d'instruction étant pleine. Le pipeline flottant étant bloqué, la première file de micro-opérations serait bloquée et on ne pourrait plus émettre de micro-opérations entières. Pour éviter cela, une solution serait d'agrandir la file de micro-opérations, mais cela la rendrait plus lente et se ferait donc au détriment de la fréquence d'horloge. Alors une solution a été d'ajouter une seconde file de micro-opérations, au lieu d'agrandir la première. [[File:Zen microarchitecture.svg|centre|vignette|upright=3|Microarchitecture Zen 1 d'AMD.]] Le passage à la microarchitecture n'a pas causé de grands changements. Le Zen 2 a ajouté une unité de calcul d'adresse, ce qui fait qu'on passe à 4 ALU, 3 AGU et 4 FPU. La fenêtre d'instruction flottante reste la même. Par contre, les fenêtres d'instruction entières changent un peu. Ou plutot devrais-je dire les fenêtres d'instruction mémoire. En effet, le Zen 2 fusionne les fenêtres d'instructions liées aux AGU en une seule fenêtre d'instruction deux fois plus grosse. Le Zen 5 a ajouté deux autres ALU entières et une unité de calcul d'adresse (6 ALU / 4 AGU) ==Les processeurs Atom d'Intel, de microarchitecture Bonnell== L'architecture de l'Atom première génération est assez simple. Son pipeline faisait 16 étages, ce qui est beaucoup. C'est un processeur 32 bits, ce qui aura son importance dans ce qui suit. Il était conçu pour être un processeur basse consommation, donc peu puissant. En conséquence, il n'a pas d'exécution dans le désordre, même s'il est superscalaire. C'était la norme à l'époque pour les processeurs basse consommation, que de faire sans exécution dans le désordre. De nos jours, les choses ont bien changée, même les processeurs basse consommation ont exécution dans le désordre, superscalarité et renommage de registres. ===Le ''front-end'' de l'Atom=== Le cache d'instruction permet de lire 8 octets par cycle, qui sont placés dans une file d'instruction, elle-même suivie par deux décodeurs. Le fait que les décodeurs lisent les instructions depuis une file d'instruction fait que les deux instructions décodées ne sont pas forcément consécutives en mémoire RAM. Par exemple, l'Atom peut décoder un branchement prédit comme pris, suivi par l'instruction de destination du branchement. Les deux instructions ont été chargées dans la file d'instruction et sont consécutifs dedans, alors qu'elles ne sont pas consécutives en mémoire RAM. Sur l'Atom, la majorité des instructions x86 sont décodées en une seule micro-opération, y compris les instructions ''load-up''. Le microcode n'est utilisé que pour une extrême minorité d'instructions et est à part des deux décodeurs précédents. L'avantage est que cela permet d'utiliser au mieux la file de micro-opération, qui est de petite taille. Mais surtout, cela permet de grandement réduire la consommation du processeur, au détriment de ses performances. Pour avoir un décodage rapide, malgré des instructions complexes, le processeur recourt à la technique du pré-décodage, qui prédécode les instructions lors de leur chargement dans le cache d'instruction. Le prédécodage lui-même prend deux cycles, là où une lecture dans le L1 d'instruction en prend 3. les défauts de cache d'instruction sont donc plus longs de deux cycles. Mais l'avantage du prédécodage est que la consommation d'énergie est diminuée. Prenez une instruction exécutée plusieurs fois, dans une boucle. Au lieu de décoder intégralement une instruction à chaque fois qu'on l'exécute, on la prédécode une fois, seul le reste du décodage est fait à chaque exécution. D'où un gain d'énergie assez intéressant. Les caches de micro-opération, qui sont capables d'exécuter une optimisation similaire, n'existaient pas encore à cette époque. ===Le chemin de données de l'Atom=== Les deux décodeurs alimentent une file de micro-opérations de petite taille : 32 µops maximum, 16 par ''thread'' si le ''multithreading'' matériel est activé. La file de micro-opérations a deux ports d'émission, ce qui permet d'émettre au maximum 2 µops par cycle. Les conditions pour cela sont cependant drastiques. Les deux instructions ne doivent pas avoir de dépendances de registres, à quelques exceptions près liées au registre d'état. Le multithreading matériel doit aussi être désactivé. Les deux instructions doivent aller chacun dans un port différent, et cela tient en compte du fait que les deux ports sont reliés à des unités de calcul fort différentes. Le tout est illustré ci-dessous. Les deux ports ont chacun une ALU simple dédiée, capable de faire des additions/soustractions, des opérations bit à bit et des copies entre registres. Mais ils ont aussi des opérations qui leur sont spécifiques. La séparation entre les deux pipelines est assez complexe. Il ne s'agit pas du cas simple avec un pipeline entier et un pipeline flottant séparés. En réalité, il y a deux pipelines, chacun capables de faire des opérations entières et flottantes, mais pas les mêmes opérations. Le premier port permet d’exécuter des opérations entières simples, une addition flottante, des comparaisons/branchements, ou une instruction de calcul d'adresse LEA. Le second port/pipeline est, quant à lui, conçu pour exécuter les instruction ''load-up'' nativement, en une seule micro-opération. Il contient toute la machinerie pour faire les accès mémoire, notamment des unités de calcul d'adresse et un cache L1 de données. A la suite du cache, se trouvent une ALU entière simple, un ''barrel shifter'', et un circuit multiplieur/diviseur. Le circuit multiplieur/diviseur est utilisé à la fois pour les opérations flottantes et entières. [[File:Intel Atom Microarchitecture.png|centre|vignette|upright=2.5|Intel Atom Microarchitecture]] Cette organisation difficile à comprendre est en réalité très efficace, très économe en circuit, tout en gardant une performance intéressante. Les instructions simples, ADD/SUB/bitwise sont supportées dans les deux pipelines. Il faut dire que ce sont des opérations courantes qu'il vaut mieux optimiser au mieux. Le processeur peut donc émettre deux opérations simples et fréquentes en même temps, ce qui augmente les performances. Les opérations plus complexes, à savoir les multiplications/divisions/décalages/rotations/manipulations de bit sont supportées dans un seul pipeline. La raison est qu'il est rare que de telles opérations soient consécutives, et qu'il n'est donc pas utile d'optimiser pour cette situation. Si les deux pipelines devaient supporter ces opérations, cela demanderait de dupliquer les circuits multiplieurs/diviseur, ce qui aurait un cout en circuit important pour un gain en performance assez faible. ===Le système d'exceptions flottantes de l'Atom=== Le processeur étant sans exécution dans le désordre, ses instructions doivent écrire dans les registres dans l'ordre du programme. En conséquence, certaines instructions doivent être retardées, leur émission doit attendre que les conditions soient adéquates. Et cela pose problème avec les opérations flottantes, vu qu'elles prennent pas mal de cycles pour s'exécuter. Imaginez qu'une instruction flottante de 10 cycles soit suivie par une instruction entière. En théorie, on doit retarder l'émission de l'instruction entière de 9 cycles pour éviter tout problèmes. Le cout en performance est donc assez important. En théorie, les instructions entières et flottantes écrivant dans des registres séparés, ce qui fait que l'on pourrait exécuter instructions entières et flottantes dans le désordre. Sauf pour les instructions de copie entre registres entier et flottants, mais laissons-les de côté. Le problème est qu'une instruction flottante peut parfois lever une exception, par exemple en cas de division par zéro, ou pour certains calculs précis. Si une exception est levée, alors l'instruction flottante est annulée, de même que toutes les instructions qui suivent, y compris les opérations entières. Ce n'est pas un problème si le processeur gère nativement les exceptions précises, par exemple avec un tampon de ré-ordonnancement. Mais l'Atom étant un processeur sans exécution dans le désordre, les instructions entières devraient être mises en attente tant qu'une instruction flottante est en cours d'exécution. Heureusement, l'Atom d'Intel a trouvé une parade. La technique, appelée ''Safe Instruction Recognition'' par Intel, est décrite dans le brevet US00525721.6A. L'idée est de tester les opérandes flottantes, pour détecter les combinaisons d'opérandes à problème, dont l'addition/multiplication peut lever une exception. Si des opérandes à problème sont détectées, on stoppe l'émission de nouvelles instructions en parallèle de l'instruction flottante et l'unité d'émission émet des bulles de pipeline tant que l'instruction flottante est en cours. Sinon, l'émission multiple fonctionne. La technique permet ainsi de ne pas écrire dans les registres entiers/flottants dans l'ordre du programme : une instruction entière peut être autorisée à s'exécuter même si elle écrit dans un registre entier avant qu'une instruction flottante délivre son résultat. ==La microarchitecture Netburst du Pentium 4== Dans cette section, nous allons voir l'architecture du processeur Pentium 4, qu'on a volontairement laissée de côté précédemment. Pourquoi un tel saut dans le temps ? Parce que le Pentium est complément à part des autres architectures Intel. Le Pentium 4 a représenté une rupture en termes de microarchitecture, qui a été un échec tellement retentissant que les processeurs suivants sont repartis sur la base du Pentium 3. Il introduisait de nombreuses nouveautés architecturales qui étaient très innovantes. Par exemple, il introduisait le renommage avec un banc de registre physique, qui a été utilisé sur tous les processeurs Intel suivants. Mais la plupart de ces innovations étaient en réalité de fausses bonnes idées, ou du moins des idées difficiles à exploiter. Par exemple, le système de pipeline à ''replay'' n'a été utilisé que sur le Pentium 4 et aucun autre processeur ne l'a implémenté. ===Un focus sur la fréquence d'horloge=== La microarchitecture du Pentium 4 a été déclinée en plusieurs versions, dont les finesses de gravure n'étaient pas les mêmes. La microarchitecture Netburst, utilisée sur le Pentium 4, utilisait un pipeline à 20 étage, augmenté à 32 sur une révision ultérieure. Il a existé quatre révisions de l'architecture : Willamette (180 nm), Northwood (130 nm), Prescott (90 nm) et Cedar Mill (65 nm). Un point important est que le Pentium 4 était prévu pour fonctionner à haute fréquence. Ses 1,5 GHz étaient impressionnants pour l'époque, les autres processeurs tournant à une fréquence proche du GigaHertzs. Pour cela, la solution retenue par Intel a été un pipeline très long, avec beaucoup d'étages. Le Pentium 4 a été décliné en plusieurs versions assez proches, chacune avec sa propre finesse de gravure qui n'ont pas toute le même pipeline. Les micro-architectures ''Willamette'' et ''Northwood'' avaient un pipeline de 20 étages, alors que les autres processeurs de l'époque avaient entre 10 et 15 étages maximum. Les micro-architectures ''Prescott'' et ''Cedar Mill'' étaient une refonte qui a fait grimper le nombre d'étages à 31 ! Du jamais vu, il s'agit d'un record pour un processeur commercial. Un pipeline aussi long permet d'exécuter beaucoup d’instructions en même temps, chacune dans un étage, mais aussi d'atteindre de hautes fréquences facilement. Le problème est qu'un pipeline avec autant d'étages a beaucoup de problèmes. Un point important est que la prédiction de branchement est cruciale. Pour rappel, la pénalité en cas de mauvaise prédiction dépend du nombre d'étages avant que le branchement soit résolu. Et les branchements sont résolus soit en fin de décodage, soit dans l'unité de calcul. C'est à dire au milieu du pipeline, soit en fin de pipeline. La pénalité en cas de mauvaise prédiction de branchement était énorme sur le Pentium 4, elle atteignait facilement 30 cycles Pour compenser, le Pentium 4 avait une prédiction de branchement très performante, pour l'époque. J'insiste sur le pour l'époque. Il utilisait un prédicteur qu'on a déjà abordé dans le chapitre sur la prédiction de branchement, précisément un prédicteur adaptatif à deux niveaux avec un historique global de 16 bits. Il avait aussi un ''Branch Target Buffer'' de 4096 entrées. Mais surtout, il intégrait une sorte de précurseur du cache de micro-opération, appelé le cache de traces, qui est détaillé dans la section suivante. ===Le cache de trace du Pentium 4=== Les décodeurs du Pentium 4 ne font pas décoder les instructions, ils mémorisent le résultat dans un cache de micro-opération un peu particulier, appelé le '''cache de trace'''. Une ligne de cache peut mémoriser 6 micro-opérations, ce qui peu sembler peu mais a été repris sur les micro-architectures suivantes. Mais le cache de trace a une grande différence avec un cache de micro-opération normal. Un cache de micro-opération normal mémorise une instruction par ligne de cache. Une instruction est décodée en plusieurs micro-instructions, qui sont enregistrées dans une ligne de cache. Si l'instruction n'utilise par les 6 micro-opérations disponibles, le reste de la ligne de cache n'est pas utilisé. Mais le Pentium 4 optimise le tout de manière ce à ce que ne soit pas le cas. Sur le Pentium 4, la contrainte du "une instruction par ligne de cache" est abandonnée. Une ligne de cache mémorise 6 micro-opérations consécutives, qui peuvent appartenir à plusieurs instructions. Par exemple, si le décodeur décode 4 instructions consécutives en 6 micro-opérations au total, alors le tout prendra une seule ligne de cache sur le Pentium 4. Et les 4 instructions consécutives n'ont même pas à être consécutives en mémoire : il peut y avoir des branchements pris entre ces instructions ! {|class="wikitable" |+ Cache de trace |- ! Ligne de cache | ADD || SUB || ADD || MOV || MUL || ''shift'' |- ! Ligne de cache | colspan="3" | ADD ''load-up'' || MUL || colspan="2" | Branch if Equal |- ! Ligne de cache | XOR || colspan="4" | POP || SUB |- ! ... | colspan="6" | ... |} Pour expliquer cela plus concrètement, nous allons devoir introduire les concepts de trace et de bloc de base. Un '''bloc de base''' (''basic block'') est une suite d'instructions sans branchement, qui est séparé par deux branchements. Le début d'un bloc de base est la destination d'un branchement, un bloc de base se termine avec un branchement. Une '''trace''' est formée en concaténant plusieurs blocs de base. Pour donner un exemple, regardez le code illustré ci-contre. Il est composé d'un bloc de base A, suivi par un bloc de base B, qui peut faire appel soit au bloc C, soit un bloc D. Un tel code peut donner deux traces : ABC ou ABD. La trace exécutée dépend du résultat du branchement qui choisit entre C et D. Le cache de trace mémorise des traces de 6 micro-opérations consécutives. Les traces sont formées en sortie des décodeurs d'instruction, par de subtiles opérations mélangeant mémorisation, décalage et concaténation. Les circuits qui construisent les traces ne sont pas connus, mais ils doivent certainement être très compliqués. toujours est-il qu'un cache de trace peut mémoriser des traces différentes, même si leur début est le même. Par exemple, prenons deux traces, composées des blocs de base A, B, C et D. La première trace est la trace ABC, la seconde est la trace ABD. Les deux traces auront chacune une ligne de cache dédiée. Une trace est réutilisable quand le premier bloc de base est identique et que les prédictions de branchement restent identiques. Pour vérifier cela, le tag du cache de traces contient l'adresse du premier bloc de base, la position des branchements dans la trace et le résultat des prédictions utilisées pour construire la trace. Le résultat des prédictions de branchement de la trace est stocké sous la forme d'une suite de bits : si la trace contient n branchements, le n-ième bit vaut 1 si ce branchement a été pris, et 0 sinon. Même chose pour la position des branchements dans la trace : le bit numéro n indique si la n-ième instruction de la trace est un branchement : si c'est le cas, il vaut 1, et 0 sinon. Si la trace est réutilisée par la suite, elle est lue depuis le cache de traces. Pour savoir si une trace est réutilisable, l'unité de chargement envoie le ''program counter'' au cache de traces, l'unité de prédiction de branchement fournit le reste des informations. Si on a un succès de cache de traces, et la trace est envoyée directement au décodeur. Sinon, la trace est chargée depuis le cache d'instructions et assemblée. Il faut signaler que le cache de trace avait sa propre unité de prédiction de branchement séparée de l'unité de prédiction de branchement normale. [[File:TraceCache.png|centre|vignette|upright=2|Cache de traces.]] Le cache de traces réduisait la longueur du pipeline en cas de succès de cache de trace. Quand les instructions étaient lues depuis le cache de trace, les étages avant le cache de trace ne sont pas utilisés, tout se passe comme s'ils étaient retirés du pipeline. C'est la même chose avec le cache de micro-opération des processeurs modernes, mais l'idée n'existait pas encore à l'époque. Le cache de trace mémorise des traces décodées, ce qui fait qu'un succès de cache de trace contournait non seulement le cache d'instruction, mais aussi les décodeurs. Le temps d'accès au cache de trace pouvait être assez élevé, même s'il était comparable au temps d'accès du cache d'instruction. Le cache de traces a depuis été remplacé par une alternative bien plus intéressante, le cache de micro-opérations, plus flexible et plus performant. Comparé à un cache de trace, la contrainte "une instruction par ligne de cache" simplifie grandement l'implémentation d'un cache de micro-opération. Ne parlons pas de la détection des succès de cache, qui demande d'utiliser les prédictions de branchement. Mais le vrai problème avec le cache de trace est tout autre. Il arrive souvent qu'une micro-opération soit présente dans plusieurs lignes de cache en raison du processus de construction des traces, chose impossible avec un cache de micro-opération. Et c'est un problème, qui réduit la capacité effective du cache de trace. Alors certes, une ligne de cache est plus remplie que sur un cache de micro-opération, on est certain que les 6 micro-opération par ligne de cache sont remplies. Mais la redondance réduit grandement cet avantage. ===L'exécution dans le désordre et le chemin de données du P4=== Le renommage de registres se fait avec un banc de registres physiques avec une table d'alias. Le Pentium 4 avait une exécution dans le désordre très limitée, basée sur la présence de deux files de micro-opération : une pour les accès mémoire, une autre pour les autres instructions. La seconde file regroupait opérations entières et flottantes, elles n'étaient pas séparées. Avec ces deux files, les instructions mémoire étaient exécutées dans l'ordre du programme, les instructions arithmétiques s'exécutaient aussi dans l'ordre du programme, mais une instruction arithmétique pouvait passer avant une instruction mémoire et inversement. L'avantage est que cela permettait de faire des lectures en avance, c'était une forme limitée de lecture non-bloquantes. Il s'agit bel et bien de deux files d'instructions, pas de fenêtres d'instruction ni de stations de réservation. Le Pentium 4 est le seul processeur commercial qui a utilisé des files de micro-opération séparées, tous les autres utilisent des fenêtres d'instruction : centralisées pour Intel, décentralisées pour AMD (en général). Le Pentium 2 et 3, bien qu'antérieurs, utilisait une station de réservation unique. Cela peut sembler être un retour en arrière, mais les files d'instructions sont bien plus larges : de 42 micro-opérations pour le Pentium 3, on passe à 120 micro-opérations pour le Pentium 4. Et vu la longueur du pipeline, qui fait qu'il y a plus d'instructions en vol, c'était une nécessité. Mais cela n'aurait pas été possible en utilisant des stations de réservation, pour des raisons de consommation électrique et/ou de budget en transistors, ce qui fait que passer à une file de micro-opération été la solution retenue. Pour les accès mémoire, le Pentium 4 utilisait donc une file de µops mémoire unique, couplée à une file d'écriture (non-représentée sur les schémas qui suivent). La file d'écriture du Pentium 4 était de 24 écritures maximum et gérait le ''Store-to-load forwarding''. Le processeur pouvait émettre une lecture et une écriture à chaque cycle. Il y avait un port d'émission pour les lectures et un autre pour les écritures, tous deux ayant chacun leur propre unité de calcul d'adresse. Le processeur contenait 3 ALU entières, 2 unités de calcul d'adresse et une FPU. La FPU était complétée par une unité pour faire des copies entre registres flottants, des opérations MOV. Pour les AGU, il y en avait une dédiée aux lectures, une autre pour les écritures. Le tout était relié aux ports d'émissions comme suit : [[File:Ports d'émission du Pentium 4.png|centre|vignette|upright=2.5|Ports d'émission du Pentium 4]] Le processeur utilisait deux réseaux de contournement séparés : un pour les opérations flottantes, un pour les opérations entières. Le réseau de contournement pour les opérations entières est aussi relié aux unités de calcul d'adresse. Jusque là, rien de surprenant, le chemin de données du processeur est assez classique. [[File:Architettura Pentium 4.png|centre|vignette|upright=3|Microarchitecture du Pentium 4.]] ===Les unités de calcul entières du Pentium 4=== Sur le Pentium 4, les ALU entières étaient cadencées à une fréquence double de celle du processeur. Les ALU entières pouvaient exécuter deux micro-opérations par cycle, ce qui fait que les ports d'émissions reliés aux ALU devaient eux aussi fonctionner à double fréquence. Pour faire la différence entre les deux fréquences, nous parlerons de fréquence/cycle processeur et de fréquence/cycle de l'ALU. Précisons que seules les ALU entières étaient à double fréquence, pas le multiplieur, pas le ''barrel shifter''. Pour simplifier, nous allons parler d'additionneur plutôt que de l'ALU entière, ce qui sera plus proche de la réalité. Et l'implémentation de l'additionneur du Pentium 4 était très innovante. L'additionneur pouvait exécuter deux additions par cycle, même si les deux additions ont une dépendance. Mais n'allez pas croire que l'implémentation était intuitive, avec un additionneur 32 bit basique très rapide. Non seulement l'additionneur fonctionnait à double fréquence, mais il était aussi pipeliné, avec un système de contournement interne ! Les additionneurs étaient pipelinées, d'une manière très simple. Une addition 32 bits était découpée en trois étapes : deux additions de 16 bits, une dernière étape pour mettre à jour le registre d'état. Pour cela, chaque additionneur était composé de deux additionneur 16 bits chacune, placées l'une après l'autre, avec un registre de pipeline entre les deux. L'additionneur prenait deux cycles d'horloge pour faire son travail : le premier cycle calculait les 16 bits de poids faible, le second calculait les 16 bits de poids fort lors du second cycle. Le tout est appelé '''addition étagée''' (''staggered add'') dans la documentation Intel. Une addition se fait donc en deux étapes, sauf que c'est compensé par le fait que l'additionneur fonctionnait à une fréquence double de celle du processeur ! Le résultat de ce fonctionnement franchement bizarre, est que les 16 bits de poids faible étaient calculés en une moitié de cycle processeur, alors que l'opération complète prenait un cycle. Deux additions consécutives s'exécutaient donc en 1 cycle et demi, alors qu'on aurait cru au premier abord que cela prendrait seulement un cycle. Si on fait les calculs, on s'apercoit que le rythme de croisière est cependant proche de 2 additions par cycle, bien qu'inférieur. 3 additions consécutives se font en deux cycles, 5 additions en 3 cycles, 7 en 4 cycles, etc. Et le Pentium 4 ajoutait un système de contournement interne à l'ALU. En clair, si une addition utilise le résultat de l'addition précédente, les deux peuvent s'exécuter en un cycle d'horloge et demi. Les 16 bits de poids faible de la première addition sont disponibles après un cycle ALU, ce qui permet de démarrer le calcul de la seconde addition au cycle suivant. ===Le ''replay pipeline''=== Le processeur est un processeur triple émission : il peut charger et décoder 3 µops par cycle, le ROB peut terminer 3 µops par cycle, etc. Pourtant, les ports d'émission peuvent émettre 6 instructions par cycle : 4 ports, dont deux à double fréquence. Une telle différence s'explique par l'usage d'un ''replay pipeline'', dont nous avons déjà parlé dans ce cours. Pour rappel, le Pentium 4 suppose que les lectures font tous un succès de cache L1. Si une opération arithmétique utilise la donnée lue comme opérande, le processeur l'émet immédiatement, l'opérande sera disponible une fois l'opération en entrée de l'ALU entière. Mais s'il s'est trompé, le processeur ré-exécute l'instruction après un temps d'attente de quelques cycles, pour se caler sur la latence du cache L2. Et si il y a un défaut de cache L2, l’instruction attend encore. Cela demande de ré-exécuter des instructions émises à tord, ce qui fait que le processeur doit avoir la capacité d'exécution pour. Ce pourquoi le processeur peut émettre 6 µops dans les unités de calcul : 3 µops normales et 3 µops ré-exécutées. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Les processeurs superscalaires | prevText=Les processeurs superscalaires | next=Les processeurs VLIW et EPIC | nextText=Les processeurs VLIW et EPIC }} </noinclude> kj26h1gwe2a1w2f78e4id7v6achbndz 772752 772751 2026-09-21T21:12:39Z Mewtow 31375 /* Les microarchitectures récentes d'Intel */ 772752 wikitext text/x-wiki Dans ce chapitre, nous allons étudier des exemples de processeurs x86, ceux présents dans nos PC. Nous n'allons pas voir les anciens processeurs comme le 286, le 386 ou le 486. Nous allons commencer avec le Pentium 1, et les processeurs commerciaux qui ont suivis. Tous les processeurs que nous allons voir dans ce chapitre sont des processeurs superscalaires. De fait, ce n'est pas pour rien si ce chapitre se situe après le chapitre sur les processeurs superscalaires. Par contre, nous allons voir que certains n'ont pas d'exécution dans le désordre. La raison est que l'exécution dans le désordre est arrivé après la superscalarité. ==Généralités sur les CPU x86 superscalaires== Avant de voir chaque processeur indépendamment des autres, nous allons devoir aborder quelques généralités. Nous allons d'abord voir que le jeu d'instruction x86 pose quelques problèmes pour la superscalarité. Et ces problèmes posent des contraintes assez fortes, avec lesquelles les concepteurs de processeurs dovient faire avec. Nous poursuivrons ensuite par un historique des processeurs Intel et AMD, histoire de donner un peu de contexte aux processeurs que nous allons étudier. ===Le jeu d'instruction x86 pose des problèmes pour la superscalarité=== Une difficulté de l'architecture x86 est qu'il s'agit d'une architecture CISC, avec tous les défauts que ça implique. Un jeu d'instruction CISC a en effet de nombreuses propriétés qui collent mal avec l'émission multiple, avec la '''superscalarité'''. Il y en a plusieurs, certaines impactent le chargement des instructions, d'autres leur décodage, d'autres l'exécution, etc. Premièrement, les instructions sont de longueur variable, entre 1 et 15 octets, ce qui complique leur chargement et leur décodage. En pratique, les processeurs chargent un bloc de 32 à 64 octets, et découpent celui-ci en plusieurs instructions. La conséquence est que l'usage d'instructions trop longues peut poser problème. Imaginez qu'un processeur charge un bloc de 16 octets et que celui-ci ne contienne qu'une seule instruction : on ne profite pas de la superscalarité. Deuxièmement, une partie des instructions est microcodée, faute de mieux. Et cela pose de sérieux challenges pour l'implémentation des décodeurs. Dupliquer le microcode demanderait trop de transistors, ce qui fait que ce n'est pas fait. À la place, il n'y a qu'un seul microcode, ce qui fait que l'on ne peut pas décoder plusieurs instructions microcodées en même temps. Il est cependant possible de profiter de la superscalarité, en décodant une instruction microcodée en parallèle d'autres instructions non-microcodées. Et heureusement, ce cas est de loin le plus fréquent, il est rare que plusieurs instructions microcodées se suivent. Troisièmement, la présence d'instructions ''load-up'', qui lisent un opérande en mémoire, peut poser problème, mais est aussi source d'optimisations assez intéressantes. En théorie, une instruction ''load-op'' est décodée en deux micro-opération : une pour lire d'opérande en RAM, l'autre pour faire l'opération arithmétique. Sauf que les processeurs x86 modernes optimisent la gestion des instructions ''load-up''. Par exemple, les premiers processeurs Atom géraient des micro-opérations de type ''load-up'', directement dans le chemin de données ! D'autres processeurs utilisent la technique de la '''micro-fusion''' pour retarder le décodage réel des instructions ''load-up'' assez loin dans le pipeline. Avec eux, une instruction ''load-op'' est décodée en une seule "macro-opération", qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. L'avantage est qu'une macro-opération ne prend qu'une seule entrée dans le tampon de ré-ordonnancement, la fenêtre d'instruction, la file de micro-opération, et les autres structures similaires. ===Un petit historique des processeurs x86 superscalaires=== Nous allons voir les processeurs Intel à part des processeurs AMD. La raison à cela est que les architectures Intel et AMD ont progressivement évolué, chacune se basant sur la précédente et l'améliorant. Il n'y a pas eu de cassure entre microarchitectures AMD, qui sont chacune la suite de la précédente. Il est donc préférable de voir les architectures AMD dans l'ordre chronologique. Par contre, Intel a eu une gigantesque cassure, avec le processeur Pentium 4. Son architecture se démarquait fortement du Pentium 3, mais elle n'a pas convaincu et a été abandonnée avec les processeurs suivants. Ce qui fait nous verrons l'architecture du Pentium 4 à part. Le Pentium 1 et 2 utilisaient la même architecture, qu'on détaillera dans ce qui suit. Les seules différences importantes étaient la fréquence, le cache, et quelques détails dans le genre. Le Pentium 3 était une nouvelle microarchitecture qui ajoutait l'exécution dans le désordre. Un an et demi plus tard, le Pentium 4 est sorti et a été un échec. Ses performances étaient peu convaincantes face au Pentium 3, et sa consommation énergétique était très importante. La conséquence est que le Pentium 4 et le Pentium 3 ont survécu pendant un long moment, beaucoup de monde préférait acheter un Pentium 3. Intel a alors amélioré les microarchitectures du Pentium 3 et du 4, indépendamment, pendant environ 7 ans. La microarchitecture du Pentium 3 a subit plusieurs micro-évolutions, chacune avec une finesse de gravure différente, afin de satisfaire les consomateurs. Les premeirs Pentium 3 avaient une finesse de gravure de 250 nm, elle a chuté à 65 sur les derniers modèles. L'architecture du Pentium 4 a fait la même chose, pour tenter de corriger ses problèmes de performance et de consommation d'énergie. Les premiers Pentium 4 avaient uen finesse de gravure de 180 nm, elle a elle aussi chutée à 65 sur les derniers modèles. Après l'échec du Pentium 4, les ingénieurs d'Intel ont repris l'architecture P6 et l'ont améliorée fortement, pour donner l'architecture Core. Les micro-processeurs suivants ont fait évoluer cette architecture progressivement, au point où elle ne ressemble plus à l'originale. L'architecture Core a laissé la place à l'architecture Nehalem, puis Sandy Bridge, puis Haswell, puis Skylake, puis Ice Lake, et Golden Cove. Il s'agit de la lignée principale, partant du Pentium 3 et continuant jusqu'à nos jours. Ces microarchitectures ont suivi un motif assez simple, appelé modèle '''tick-tock'''. Chaque microarchitecture était déclinée en deux versions, la seconde ayant une finesse de gravure réduite. En parallèle, Intel a travaillé sur des processeurs basse performance et basse consommation, avec une microarchitecture très différente. Les processeurs Atom de microarchitecture Bonnel, pour être ensuite remplacés par les microarchitectures Silvermont, puis Goldmont et Gracemont. Ces microarchitectures ont évolué en parallèle de la lignée principale, il s'agit d'une lignée secondaire. Le tout est résumé dans ce schéma ci-dessous. [[File:IntelProcessorRoadmap-4v.svg|centre|vignette|upright=2.5|Roadmap des processeurs Intel, qui servira de structure pour la suite du chapitre]] De nos jours, les processeurs Intel utilisent les deux types de micro-architectures en même temps. Les CPU Intel modernes disposent de deux types de coeurs : les coeurs P et le coeurs E. Leur nom signifie "Performance" et "Efficient", qui trahissent leur but. Les coeurs P utilisent la microarchitecture de la lignée principale, qui est conçue pour la performance. Les coeurs E, quant à eux, utilisent les micro-architectures basse consommation de la lignée secondaire. ==Les processeurs x86 d'Intel, la lignée principale== Pour commencer, nous allons voir les processeurs Intel. N'y voyez pas du favoritisme derrière ce choix, la justification est toute autre. Si je commence par Intel, c'est pour commencer avec les Pentium 1 et 2, qui sont plus simples que tous les autres. Il s'agit en effet de processeurs superscalaires, mais sans exécution dans le désordre. L’absence d'exécution dans le désordre les rend bien plus simples à étudier que les autres, ce qui en fait un bon point de départ pour ce chapitre. Le successeur du Pentium 2 a intégré l'exécution dans le désordre, ce qui fait que les Pentium 1 et 2 sont les seuls processeurs superscalaires ''in-order''. Pour la concurrence, AMD n'a pas produit de processeurs ''in-order'', tous les processeurs produits par AMD intègrent l'exécution dans le désordre. A la rigueur, il y a bien les processeurs Atom de la lignée secondaire d'Intel. Cependant, même s'ils sont bien des CPU ''in-order'', leur architecture est assez compliquée. En comparaison, le Pentium est une vieille architecture, qui se débrouillait avec peu de transistors et était donc bien plus simple que celle de l'Atom. ===Le Pentium 1/MMX et les pipelines U/V=== Le processeur Pentium d'Intel avait un pipeline de 5 étages : un étage de chargement/prédiction de branchement, deux étages de décodage, un étage d'exécution et un dernier étage pour l'écriture dans les registres. Le Pentium 1 était un processeur double émission, intégrant deux pipelines nommés U et V. Chose importante, les deux pipelines n'étaient pas identiques. Le pipeline U pouvait exécuter toutes les instructions, mais le pipeline V était beaucoup plus limité. Par exemple, seul le pipeline U peut faire des calculs flottants, le pipeline V ne fait que des calculs entiers et des branchements. Les deux pipelines disposaient d'une unité de calcul entière, identique dans les deux pipelines. Mais le pipeline U incorporait un circuit multiplieur/diviseur et d'un ''barrel shifter''. L'unité flottante était sur le port d'émission du pipeline U, idem pour l'unité de calcul vectoriel MMX sur le Pentium MMX. Les deux pipelines avaient chacun une unité de calcul d'adresse, mais ils n'étaient pas identiques : celle du pipeline V ne gérait que l’instruction LEA, celle du pipeline U gérait tous les calculs d'adresse. {|class="wikitable" |- ! Pipeline U ! Pipeline V |- | ALU entière | ALU entière |- | Multiplieur/diviseur | |- | ''Barrel Shifter'' | |- | AGU complexe | AGU simple (opération LEA) |- | FPU | |- | Unité SIMD | |} Les deux pipelines géraient les opérations bit à bit, les additions, les soustractions et les comparaisons. Les autres instructions ne sont exécutables que dans le pipeline U. Pour être plus précis, les deux pipelines supportaient les instructions suivantes, ce qui fait qu'on pouvait en faire deux en même temps : * Les instructions arithmétiques INC, DEC, ADD, SUB ; * l'instruction de comparaison CMP ; * les instructions bit à bit AND, OR, XOR ; * l'instruction de calcul d'adresse LEA ; * l'instruction MOV (dépend du mode d'adressage) ; * les instructions de gestion de la pile PUSH et POP (dépend du mode d'adressage) ; * l'instruction NOP, qui ne fait rien. Il faut noter qu'il y a cependant quelques restrictions, beaucoup de paires d'instructions sont interdites. La plupart interdisent au pipeline V de faire quoique ce soit quand une opération particulière est émise dans le pipeline U. Par exemple, si le pipeline U exécute une multiplication ou une division, le processeur ne peut pas exécuter une opération dans le pipeline V. Et c'est pareil avec les branchements : si un branchement est émis dans le pipeline U, l'instruction suivant le branchement n'est pas émise dans le pipeline V, pour éliminer les dépendances de contrôle. De même, si le pipeline U exécute une opération flottante, le pipeline V ne pourra rien exécuter. La seule exception est l'instruction FCXH, qui échange deux registres flottants. [[File:Intel Pentium arch.svg|centre|vignette|upright=2.5|Microarchitecture de l'Intel Pentium MMX. On voit que certaines unités de calcul sont dupliquées.]] Un choix assez intéressant a été fait pour le cache de données. Nous avions vu dans le chapitre sur les CPU superscalaires que la superscalarité a un impact sur l'unité mémoire. Il y a alors deux implémentations. La première ne fait rien, l'unité mémoire ne change pas, et le processeur ne peut pas faire deux accès mémoire simultanés. La seconde duplique l'unité mémoire et les ports de lecture/écriture du cache, ce qui autorise des accès mémoire simultanés. Le Pentium 1 utilise une solution intermédiaire. Les ingénieurs d'Intel étaient partis à la base sur un cache totalement double port, pour obtenir des performances maximales. Mais diverses simulations et observations les ont fait changer d'avis. Les simulations ont montré qu'il est "rare" que les deux pipelines aient besoin de lire/écrire dans le cache en même temps. Et ils ont optimisé le cache de donnée pour en tenir compte. Le cache de données est partiellement multiport : simple port sur certains aspects, double port sur d'autres. Le cache est un cache splité, à savoir que les données et les ''tags'' sont séparés dans des mémoires séparées. La mémoire pour les ''tags'' est multiport, ce qui permet d'interroger les ''tags'' du cache deux fois par cycle. Un port est relié au pipeline u, un autre au pipeline V. Mais pour les données, le cache n'a qu'un seul port pour lire/écrire des données. Impossible donc de lire deux données en même temps, pour alimenter les deux pipelines. Il utilise cependant 8 banques permet d'accélérer des accès mémoire proches dans le temps, mais dans des cycles d'horloge différents. Entre les deux mémoires, il y a un circuit qui détecte les conflits, à savoir les situations où les deux pipelines accèdent en même temps au cache. S'ils veulent lire/écrire une donnée dans le même cycle, ce qui est impossible avec un seul port, le pipeline U a la priorité et le pipeline V attend le cycle suivant. Le circuit détecte aussi les conflits de banque, à savoir quand un pipeline accède à une banque en cours d'accès par l'autre pipeline (on rappelle qu'un accès au cache prend plusieurs cycles). Le circuit détecte aussi certaines dépendances mémoires, à savoir des accès consécutifs à la même adresse. : La TLB du processeur est aussi totalement double port. ===La microarchitecture P6 du Pentium 2/3=== Le Pentium 3 utilisait la '''microarchitecture P6''', qui a été dérivée dans de nombreuses variantes, dont les finesses de gravure n'étaient pas les mêmes. Il introduit une exécution dans le désordre simple, avec une fenêtre d'instruction centralisée, avec renommage dans le désordre dans le ROB (tampon de ré-ordonnancement), commandé par une table d'alias. C'était un processeur triple émission, soit une instruction de plus que la double émission du Pentium 1. Le pipeline passe de 5 étage sur le Pentium à 14 - 12 étages, dont le détail est le suivant : * Prédiction de branchement, deux cycles ; * Chargement des instructions, trois cycles ; * Décodage de l'instruction, deux cycles ; * Renommage de registre, un cycle ; * Copie des opérandes dans le tampon de ré-ordonnancement (lié au renommage de registre dans le ROB) ; * Dispath dans ou depuis la station de réservation. * Exécution de l'instruction ; * Écriture du résultat dans le ROB ; * Écriture dans le banc de registre physique. Les instructions sont chargées par blocs de 16 octets, avec un système de fusion de blocs pour gérer les instructions à cheval sur deux blocs. Lors d'un branchement, deux blocs doivent être chargés si l'instruction de destination n'est pas alignée sur 16 octets et cela cause un délai de un cycle d'horloge. Le décodage des instructions x86 était géré par plusieurs décodeurs. Il y avait trois décodeurs : deux décodeurs simples, et un décodeur complexe. Les décodeurs simples décodaient les instructions les plus fréquentes, mais aussi les plus simples, qui étaient décodées en une seule micro-opération. Les instructions CISC complexes étaient gérées uniquement par le décodeur complexe, basé sur un microcode, qui pouvait fournir jusqu'à 4 micro-opérations par cycle. Le tout est résumé avec la règle 4-1-1. La toute première instruction chargée depuis la file d'instruction va dans le premier décodeur simple. Si jamais le décodeur ne peut pas décoder l'instruction, l'instruction est redirigée dans un autre décodeur, avec un délai d'un cycle d'horloge. Les stations de réservations étaient regroupées dans une structure centralisée, en sortie de l'unité de renommage. Elles avaient 5 ports d'émission, qui étaient sous-utilisés en pratique. Niveau ALU, on trouve deux ALUs entières, une flottante, une unité pour les instructions SSE et autres, et trois unités pour les accès mémoire (regroupées en une seule unité dans le schéma ci-dessous). Les unités mémoire regroupent une unité de calcul d'adresse pour les lectures, une autre pour les écritures, et une unité pour la gestion des données à écrire. Les unités de calcul d'adresse sont des additionneurs à 4 opérandes, complétement différents des ALU entières. Les ALU entières sont deux unités asymétriques : une ALU simple, et une ALU complexe incorporant un multiplieur. Les deux peuvent exécuter des opérations d'addition, soustraction, comparaison, etc. [[File:P6 func diag.png|centre|vignette|upright=2|P6 func diag]] Les premiers Pentium 3 n'avaient pas de cache L2 dans le processeur, celui-ci était sur la carte mère. Mais il a été intégré dans le processeur sur la seconde version du Pentium 3, la version Coppermine. Le Pentium 3 a servi de base aux microarchitectures d'Intel qui ont suivi. Les changements à chaque nouvelle génération sont assez mineurs : la prédiction de branchement est améliorée, la taille des stations de réservation et du ROB augmente, idem avec les autres structures liées à l'exécution dans le désordre. Les processeurs Intel ont conservé une fenêtre d'instruction centralisée, alors qu'AMD utilise une autre méthode, comme nous allons le voir dans ce qui suit. Les seuls changements notables sont est le passage à un renommage dans le ROB à un renommage à banc de registre physique, ainsi que l'introduction du cache de micro-opération. Et ce sont des modifications qu'AMD a aussi faites, celle-ci étant clairement une bonne idée pour toutes les micro-architectures avec un budget en transistor suffisant. Il est intéressant de garder cela en tête, car une bonne partie des améliorations de chaque micro-architecture proviendra de là. ===La microarchitecture Core=== La '''microarchitecture Core''' fait suite au Pentium 4, mais reprend en fait beaucoup d’éléments du Pentium 2 et 3. Elle utilise la station de réservation unique avec renommage dans le ROB, provenant du Pentium 2/3. Elle supporte aussi les optimisations des opérations ''load-up'', avec notamment un support des macro-opérations mentionnées plus haut. Les améliorations sont assez diverses, mais aussi assez mineures. * Le processeur incorpore un cache L2, en plus des caches L1 déjà présents auparavant. * La prédiction de branchement a été améliorée avec notamment l'ajout d'une ''Fetch Input Queue''. * L'architecture Core passe à la quadruple émission, soit une instruction de plus que sur le Pentium 2 et 3. Pour cela, un quatrième décodeur est ajouté, il s'agit d'un décodeur simple qui ne fournit qu'une seule micro-opération en sortie. * Un ''stack engine'' et un ''Loop Stream Detector'' ont été ajoutés, ainsi que le support de la macro-fusion qui fusionne une instruction de test et le branchement qui suit en une seule micro-opération. * Les techniques de désambiguïsation mémoire sont implémentées sur cette micro-architecture. Il y a quelques modifications au niveau de l'unité de chargement. La file d'instruction a toujours ce système de fusion de blocs, sauf que les branchements ne causent plus de délai d'un cycle lors du chargement. La file d'instruction est suivie par un circuit de prédécodage qui détermine la taille des instructions et leurs frontières, avant de mémoriser le tout dans une file de 40 instructions. La station de réservation dispose de 6 ports d'émission, mais on devrait plutôt dire 5. Sur les 5, il y en a un pour les lectures, un pour les écritures. Les deux sont reliées à une ''Load/Store Queue'', appelée ''Memory Ordering Buffer''. Elle est elle-même reliée au cache de données par deux ports : un port de lecture et un port d'écriture. Les trois ports d'émission restants sont connectés aux unités de calcul. Les trois ports récents sont chacun reliés à une ALU entière. L'additionneur flottant est connecté au port 1, alors que le multiplieur/diviseur flottante est connecté au port 0. Le fait de mettre les deux sur des ports différents permet d'émettre une addition et une multiplication flottant simultanément. Le multiplieur entier est relié au second port d'émission, celui sur lequel se trouve l'additionneur flottant. Le résultat que le processeur peut émettre un mix d'opérations flottantes et entière assez varié. [[Image:Intel Core2 arch.svg|centre|vignette|upright=2|Intel Core microarchitecture]] ===Les microarchitectures Sandy Bridge and Ivy Bridge=== Les micro-architectures suivant la micro-architecture Core ont introduit quelques grandes modifications : le passage à un renommage à banc de registre physique, l'ajout d'un cache de micro-opérations (et d'un ''Loop Stream Detector''). L'ajout du cache de micro-opérations est un gros changement, particulièrement avec le jeu d’instruction x86. Le décodage des instructions est lent, couteux en énergie. Mais avec l'introduction du cache de micro-opération, la majorité des micro-opérations est non pas décodée, mais lue depuis le cache de micro-opérations. Les décodeurs décodent les instructions pas encore exécutées, mais les exécutions suivantes sont lues depuis le cache de micro-opérations. Et vu la grande présence de boucles, le cache de micro-opérations est l'alimentation principale du pipeline. Les décodeurs servent surtout à alimenter le cache de micro-opérations, parfois décoder quelques instructions isolées exécutées de-dehors de boucles, pas plus. Concrètement, ils servent pour 10 à 20% des micro-opérations exécutées. Intel a d'ailleurs reflété ce fait dans sa terminologie. Intel distingue deux voies de chargement : le ''legacy pipeline'' et le cache de micro-opérations. L'unité de chargement et les décodeurs sont regroupés dans la voie du ''legacy pipeline''. Le cache de micro-opérations est complété avec un ''Loop Stream Detector'', placé après le cache en question. Les décodeurs et le cache de micro-opérations alimentent une file de micro-opérations, située juste avant l'étage de renommage de registres. La file de micro-opérations sert en quelque sorte de tampon entre l'étage de "décodage" et celui de renommage. Le ''Loop Stream Detector'' utilise cette file de micro-opérations comme d'un cache lorsqu'une boucle est détectée. Les micro-opérations de la boucle sont lue depuis la file de micro-opérations, pour être envoyée au renommeur de registres. L'avantage est que le cache de micro-opérations et/ou les décodeurs sont mis en pause et clock-gatés lorsqu'une boucle s'exécute, ce qui réduit la consommation du processeur. Le ''Loop Stream Detector'' et le cache de micro-opération ont globalement le même effet : désactiver tout ce qui est avant, le ''Loop Stream Detector'' appliquant cette méthode au cache de micro-opération lui-même.. Voyons maintenant quelles sont les micro-architectures qui implémentent ces optimisations. Les microarchitectures '''Sandy Bridge''' and '''Ivy Bridge''' sont similaires à l'architecture Core, si ce n'est pour le passage à un renommage à banc de registre physique, et l'ajout d'un cache de micro-opérations. Le nombre de ports d'émission passe à 7, avec 4 pour les instructions arithmétiques (flottantes comme entière), 2 pour les lectures, et un pour les écritures (en fait deux, avec un pour le calcul d'adresse, l'autre pour la donnée à écrire). Pour le reste, rien ne change si ce n'est la prédiction de branchement Les architectures '''Haswell''' et '''Broadwell''' ont ajouté quelques unités de calcul, élargit la sortie du cache de micro-opérations. Un port d'émission pour opération entières a été ajouté, de même qu'un port pour les accès mémoire. Le processeur passe donc à 8 ports d'émission, ce qui permet d'émettre jusqu'à 8 micro-opérations, à condition que le cache de micro-opération suive. Pour le reste, le processeur est similaire aux architectures précédentes, si ce n'est que certaines structures grossissent. L'architecture '''Skylake''' réorganise les unités de calcul et les ports d'émission pour gagner en efficacité. Pour le reste, les améliorations sont mineures. A la rigueur, l'unité de renommage de registre ajoute des optimisations comme l'élimination des MOV, les idiomes liés aux opérations avec zéro, etc. ===Les microarchitectures récentes d'Intel=== Les architectures '''Ice Lake''' et '''Tiger Lake''' passent de quadruple émission à la pentuple émission. Par contre, le processeur utilise toujours 4 décodeurs. Mais les micro-opérations étant émises depuis le cache de micro-opérations, ce n'est pas un problème pour la pentuple émission. Le processeur peut parfaitement émettre 5 micro-opérations en même temps, si elles sont lues depuis le cache de micro-opérations. Là encore, on voit à quel point le cache de micro-opération découple ce qu'il y avant de ce qu'il y a après. La microarchitecture '''Golden Cove''' altère les décodeurs et l'unité de chargement. Sur toutes les générations précédentes, on reste sur une unité de chargement qui charge 16 octets à la fois et il y a toujours 4 décodeurs identiques aux générations précédentes. Golden Cove passe à 6 décodeurs simples, et double la taille du chargement qui passe à 32 octets. Une telle stagnation sur les unités de chargement et de décodage s'explique encore une fois par la présence du cache de micro-opération fait que ce n'est pas trop un problème. Tout ce qui précède le cache de micro-opérations n'a pas de raison d'évoluer, car ce cache est très puissant. Niveau unités de calcul, le CPU a pas moins de 5 ALU entières, deux ''barrel shifters'', un multiplieur et deux unités de branchements. Le tout est répartit sur 5 ports d'émission. Pour les unités mémoire, il y a trois unités LOAD pour les lectures et deux unités STORE pour les écritures. Il n'y a plus d'unité flottante proprement dite, mais une unité SIMD qui est capable de faire plusieurs calculs flottants, qu'on ne détaillera pas ici, car nous n'avons pas encore vu les techniques de SIMD. [[File:Golden Cove.png|centre|vignette|upright=3|Golden Cove]] ==Un étude des microarchitectures superscalaires x86 d'AMD== Les architectures Intel ont évolué progressivement, sans grandes cassure. Il y a une continuité presque initerrompue entre l'architecture du Pentium 2 et les architectures modernes. Intel a fait des améliorations mineures à chaque nouvelle micro-architecture, si on omet le passage à un renommage à banc de registre physique et l'ajout du cache de micro-opération. A l'opposé, les architectures AMD ont eu de nombreuses cassures dans la continuité où AMD a revu sa copie de fond en comble. Étudier ces architectures demande de voir trois choses séparément : le ''front-end'' qui regroupe l'unité de chargement et les décodeurs, le ''back-end'' qui gère l'exécution dans le désordre et les unités de calcul, et le sous-système mémoire avec les caches et la ''Load Store Queue''. Leur étude sera plus ou moins séparée dans ce qui suit, pour chaque classe d'architecture. ===La première génération de CPU AMD : les architectures K5, K6, K7, K8 et K10=== La première génération de processeurs AMD est celle des architectures K5, K6, K7, K8 et K10. Il n'y a pas de K9, qui a été abandonné en cours de développement. Les processeurs K5 et K6 portent ce nom au niveau commercial. Par contre, les processeurs d'architecture K7 sont aussi connus sous le nom d''''AMD Athlon''', les AMD K8 sont connus sous le nom d''''AMD Athlon 64''', et les architecture K10 sont appelées les '''AMD Phenom'''. Comme le nom l'indique, l'architecture K8 a introduit le 64 bits chez les processeurs AMD. Elles ont une architecture assez similaire pour ce qui est du chargement et des caches. Toutes disposent d'au minimum un cache L1 d'instruction et d'un cache L1 de données. Le K5 n'avait que ces caches, mais un cache L2 a été ajouté avec le K7, puis un L3 avec le K10. L'AMD K5 avait une TLB unique, mais les processeurs suivants avaient une TLB pour le L1 d'instruction et une autre pour le L1 de données. Idem pour le cache L2, avec deux TLB : une pour les données, une pour les instructions. Les caches L1/L2 sont de type exclusifs, à savoir que les données dans le L1 ne sont pas recopiées dans le L2. Le cache L2 est précisément un cache de victime, qui mémorise les données/instructions, évincées des caches L1 lors du remplacement des lignes de cache. L'introduction du cache L2 a entrainé l'ajout de deux TLB de second niveau : une L2 TLB pour les données et une autre pour les instructions. Les architectures K8 et K10 ont ajouté un cache L3, avec un accès indirect à travers l'interface avec le bus. : L'AMD K7 originel, aussi appelée Athlon classique, n'avait pas de cache L2, mais celui-ci était placé sur la carte mère et fonctionnait à une fréquence moitié moindre de celle du CPU. L'Athlon Thunderbird, puis l'Athlon XP, ont intégré le cache L2 dans le processeur. {|class="wikitable" |- ! Architecture AMD ! colspan="5" | Caches |- | rowspan="2" | K5 | L1 instruction || L1 données || colspan="3" | |- | colspan="2" | TLB unique || colspan="3" | |- | colspan="4" | |- | rowspan="2" | K6 | L1 instruction || L1 données || colspan="3" | L2 unifié |- | TLB L1 instruction || TLB L1 données || colspan="3" | |- | colspan="6" | |- | rowspan="2" | K7, K8 | L1 instruction || L1 données || colspan="2" | L2 unifié || |- | TLB L1 instruction || TLB L1 données || TLB L2 instruction || TLB L2 données || |- | colspan="6" | |- | rowspan="2" | K10 | L1 instruction || L1 données || colspan="2" | L2 unifié || L3 |- | TLB L1 instruction || TLB L1 données || TLB L2 instruction || TLB L2 données || |} Fait important, les architectures K5 à K10 utilisent la technique du '''prédécodage''', où les instructions sont partiellement décodées avant d'entrer dans le cache d'instruction. Le prédécodage facilite grandement le travail des décodeurs d'instruction proprement dit. Par contre, le prédécodage prend de la place dans le cache L1 d'instruction, une partie de sa capacité est utilisé pour mémoriser les informations prédécodées. C'est donc un compromis entre taille du cache et taille/rapidité des décodeurs d'instruction. Sur les architectures K5 et K6, le prédécodage précise, pour chaque octet, si c'est le début ou la fin d'une instruction, si c'est un octet d'opcode, en combien de micro-opérations sera décodée l'instruction, etc. A partir de l'AMD K7, le prédécodage reconnait les branchements inconditionnels. Lorsqu'un branchement inconditionnel est pré-décodé, le pré-décodage tient compte du branchement et continue le pré-décodage des instructions à partir de la destination du branchement. Le système de prédécodage est abandonnée à partir de l'architecture Bulldozer, qui suit l'architecture K10. La prédiction de branchement de ces CPU tire partie de ce système de pré-décodage, à savoir que les prédictions de branchement sont partiellement mémorisées dans les lignes de cache du L1 d'instruction. Par exemple, l'AMD K5 se passe de ''Branch Target Buffer'' grâce à cela. Si une ligne de cache contient un branchement, elle mémorise l'adresse de destination de ce branchement, en plus des bits de pré-décodage. Si il y a plusieurs branchements dans une ligne de cache, c'est l'adresse de destination du premier branchement pris dans cette ligne de cache qui est mémoirsée. Un défaut de cette approche est que si le branchement n'est pas dans le L1 d'instruction, aucune prédiction de branchement ne peut être faite et le préchargement ne peut pas fonctionner. C'est une limitation que n'ont pas les BTB découplées du cache L1 : elles peuvent prédire un branchement qui a été évincé dans le L2 ou le L3, tant que l'entrée associée est dans le BTB. Les prédictions peuvent même servir à précharger les instructions utiles. [[File:Comparaison du chargement de l'AMD K5 et K6.png|centre|vignette|upright=2|Comparaison du chargement de l'AMD K5 et K6]] Au niveau du décodage, on trouve de nombreuses différences entre les premières architectures AMD. L'AMD K5 contient 4 décodeurs hybrides, afin de décoder 4 instructions par cycles. Le K5 a quatre décodeurs simples couplés à 4 décodeurs complexes avec chacun un accès au micro-code. Une instruction peut donc passer par a donc deux voies de décodage : un décodage rapide et simple pour les instructions simples, un décodage lent et passant par le microcode pour les instructions complexes. Pour décoder 4 instructions, les deux voies sont dupliquées en 4 exemplaires, ce qui a un cout en circuits non-négligeable. L'AMD K6 utilise moins de décodeurs et ne peut que décoder deux instructions à la fois maximum. Par contre, il fournit en sortie 4 micro-opérations. Il intègre pour cela deux décodeurs simples, un décodeur complexe et un décodeur micro-codé. Un décodeur simple transforme une instruction simple en une ou deux micro-opérations. Il est possible d'utiliser les deux décodeurs simples en même temps, afin de fournir 4 micro-opérations en sortie du décodeur. Les deux autres décodent une instruction complexe en 1 à 4 micro-opérations. Si jamais la ou les deux instructions sont décodées en 1, 2 ou 3 micro-opérations, les micro-opérations manquantes pour atteindre 4 sont remplies par des NOPs. Pour le K7 et au-delà, le processeur dispose de décodeurs séparées pour les instructions micro-codées de celles qui ne le sont pas. Le processeur peut décoder jusqu’à 3 instructions par cycle. Le décodage d'une instruction microcodée ne peut pas se faire en parallèle du décodage non-microcodé. C'est soit le décodeur microcodé qui est utilisé, soit les décodeurs câblés, pas les deux en même temps. Le décodage d'une instruction prend 4 cycles. Les instructions non-microcodées sont décodées en une seule micro-opération, à un détail près : le CPU optimise la prise en charge des instructions ''load-up''. La différence entre le K6 et le K7 s'explique par des optimisations des instructions ''load-up''. Sur le K6, les instructions ''load-up'' sont décodées en deux micro-opération : la lecture en RAM, l'opération proprement dite. Mais sur le K7, une instruction ''load-up'' est décodée en une seule micro-opération. En conséquence, les décodeurs simples sont fortement simplifiés et le décodeur complexe disparait au profit d'un microcode unique. [[File:Décodage sur le K5 et le K5.png|centre|vignette|upright=3|Décodage sur le K5 et le K5]] ====Les microarchitectures K5 et K6 d'AMD==== Les deux premières architectures étaient les architectures K5 et K6, l'architecture K6 ayant été déclinée en quatre versions, nommées K6-1, K6-2, et K-3, avec une version K6-3 bis. Elles sont regroupées ensemble car elles ont beaucoup de points communs. Par exemple, tout ce qui a trait au chargement et au cache était similaire, de même que les unités de calcul. Les deux architectures avaient n'avaient pas de cache L2 et devaient se contenter d'un cache L1 d'instruction et d'un cache L1 de données. L'AMD K5 incorpore une TLB unique, alors que le K6 utilise des TLB séparées pour le cache d'instruction et le cache de données. Une différence entre l'architecture K5 et K6 est que la première utilise des caches normaux, alors que la seconde utilise des ''sector caches''. Les deux architectures disposaient des unités de calcul suivantes : deux ALU entières, une FPU, deux unités LOAD/STORE pour les accès mémoire, une unité de branchement et une ou plusieurs unités SIMD. Une organisation classique, donc. Pour les unités entières, il y avait deux ALU simples, un ''barrel shifter'' et un diviseur. Il n'y a pas d'erreur, le processeur incorpore un circuit diviseur, mais pas de circuit multiplieur. La raison est que la multiplication est réalisée par la FPU ! En effet, le multiplieur flottant de la FPU intègre un multiplieur entier pour multiplier les mantisses, qui est utilisé pour les multiplications entières. La même technique a été utilisée sur l'Atom, comme vu plus haut. Le tout était alimenté par deux ports d'émission, appelés ports X et Y. Sur l'architecture K5, le ''barrel shifter'' et le diviseur sont des ports différents. {|class="wikitable" |+ AMD K5 |- ! Port X ! Port Y |- | ALU simple | ALU simple |- | ''Barrel Shifter'' | Diviseur |} Sur l'architecture K6, le ''barrel shifter'' et le diviseur sont sur le même port. {|class="wikitable" |+ AMD K6 |- ! Port X ! Port Y |- | ALU simple | ALU simple |- | | ''Barrel Shifter'' |- | | Diviseur |} Niveau unités mémoire, le K5 avait deux unités LOAD/STORE, chacune capable de faire lecture et écriture. Par contre, la ''store queue'' n'a qu'un seul port d'entrée, ce qui fait que le processeur peut seulement accepter une écriture par cycle. Le processeur peut donc émettre soit deux lectures simultanées, soit une lecture accompagnée d'une écriture. Impossible d'émettre deux écritures simultanées, ce qui est de toute façon très rare. L'architecture K6 utilise quant à elle une unité LOAD pour les lectures et une unité STORE pour les écritures. Ce qui permet de faire une lecture et une écriture par cycle, pas autre chose. Niveau unités SIMD, l'architecture K7 n'avait qu'une seule unité SIMD, placée sur le port d'émission X. L'architecture K8 ajouta une seconde unité SIMD, sur l'autre port d'émission entier. De plus, trois ALU SIMD ont été ajoutées : un décaleur MMX, une unité 3DNow!, une unité mixte MMX/3DNow. Elles sont reliées aux deux ports d'émission entier X et Y ! Elles ne sont pas représentées ci-dessous, par souci de simplicité. [[File:Unité de calcul des processeurs AMD K5 et K6.png|centre|vignette|upright=2|Unité de calcul des processeurs AMD K5 et K6. les unités sur la même colonnes sont reliées au même port d'émission.]] Si les unités de calcul et le chargement sont globalement les mêmes, les deux architectures se différencient sur l'exécution dans le désordre. L'AMD K5 utilise du renommage de registre dans le ROB avec des stations de réservation. Par contre, l'AMD K6 utilise une fenêtre d'instruction centralisée. De plus, son renommage de registre se fait avec un banc de registre physique. L'architecture AMD K5 utilisait de deux stations de réservation par unité de calcul, sauf pour les deux unités mémoire partageaient une station de réservation unique (deux fois plus grande). Les stations de réservation sont cependant mal nommées, vu que ce sont en réalité des mémoire FIFO. Une micro-opération n'est émise que si elle est la plus ancienne dans la FIFO/station de réservation. Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique. Le tampon de ré-ordonnancement faisait seulement 16 instructions. [[File:AMD K5.jpg|centre|vignette|upright=3|AMDK5 Diagramme.]] L'architecture K6 remplace les stations de réservations par une fenêtre d'instruction centralisée. Les 4 micro-opérations renommées sont écrites dans la fenêtre d'instruction par groupe de 4, NOP de ''padding'' inclus. La fenêtre d'instruction centralisé contient 24 micro-opérations, groupées en 6 groupes de 4 micro-opérations, avec potentiellement des NOP dedans suivant le résultat du décodage. L'avantage est que l'implémentation de la fenêtre d'instruction est simple. La fenêtre d'instruction centralisée permettait d'émettre 6 micro-opérations en même temps (une par unité de calcul/mémoire). Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique. Le processeur utilisait un renommage avec un banc de registre physique. Le banc de registre physique pour les entiers contenait 48 registres, dont 24 étaient des registres architecturaux et 24 étaient des registres renommés. Sur les 24 registres architecturaux, 16 avaient une fonction de ''scratchpad'' que les ''datasheets'' d'AMD ne détaillent pas, les 8 restants étaient les registres généraux EAX, EBX, etc. [[File:AMD K6 Little foot & Modl 6.png|centre|vignette|upright=3|AMD K6 original.]] ====Les microarchitectures K7, K8 et K10 d'AMD==== Les microarchitectures suivantes sont les architectures K7, K8 et K10. Les architectures K7, K8 et K10 sont assez similaires. La différence principale entre le K7 et le K8 est le support du 64 bits. Les apports du K10 sont la présence d'un cache L3, d'une unité de calcul supplémentaire et d'améliorations de la prédiction de branchement. La taille de certains caches a été augmentée, de même que la largeur de certaines interconnexions/bus. L'architecture K7 des processeurs Athlon utilisait le renommage de registre, mais seulement pour les registres flottants, pas pour les registres entiers. Le ranommeg des registres flottants étaient réalisé via un banc de registres physique, ne contenant que des registres flottants. Les architectures K8 et K10 utilisent le renommage de registres pour tous les registres, entiers comme flottants. Par contre, le renommage de registre n'est pas réalisé de la même manière pour les registres entiers et flottants. Les registres entiers sont renommés dans le tampon de ré-ordonnancement, comme c'était le cas sur les architectures Intel avant le Pentium 4. Par contre, les registres flottants sont renommés grâce à un banc de registre physique. Le K8 est donc un processeur au renommage hybride, qui utilise les deux solutions de renommage principales. A partir du K7, le CPU optimise la prise en charge des instructions ''load-up''. Les instructions ''load-op'' sont appelées des macro-opérations dans la terminologie d'AMD, et aussi d'Intel. L'idée est que les instructions ''load-up'' sont décodées en micro-opérations intermédiaires. Elles sont propagées dans le pipeline comme étant une seule micro-opération, jusqu'à l'étage d'émission. Lors de l'émission, les instructions ''load-up'' sont scindées en deux micro-opérations : la lecture de l'opérande, puis l'opération proprement dite. Faire ainsi économise des ressources et optimise le remplissage du tampon de ré-ordonnancement, des fenêtres d'instructions, des stations de réservation, etc. Le tampon de réordonnancement est combiné avec divers circuits en charge de l'exécution dans le désordre, dans ce qui s'appelle l'''instruction control unit''. Il contient de 72 à, 84 instructions, qui sont regroupées en groupes de 3. Là encore, comme pour le K5 et le K6, le tampon de réordonnancement tient compte de la sortie des décodeurs. Les décodeurs fournissent toujours trois micro-opérations par cycle, quitte à remplir les vides par des NOP. Le tampon de réordonnancement reçoit les micro-opérations, NOP inclus, par groupes de 3, et est structuré autour de ces triplets de micro-opération, y compris en interne. Pour ce qui est de l'unité mémoire, elle est précédée par une file de µops mémoire, qui émet les accès mémoire dans l'ordre du programme. Elle est souvent qualifiée de ''Load-Store Queue'', mais ce n'est pas la terminologie que nous utilisons dans ce cours. La file de micro-opération lire/écrire 64 bits par cycle depuis le cache L1, ce qui fait un seul accès au cache par cycle. La file de µops mémoire est appelée la ''Pre-Cache Queue''. Si au vu de son nom, vous avez deviné qu'il y avait une ''Post-Cache Queue''. Elle mémorise les lectures/écritures émises, mais qui ont levé un défaut de cache L1. Elle ne fait pas partie de la file de µops mémoire proprement dite. Les architectures K7, K8 et K10 ont des unités de calcul très similaires. Concrètement, il y a trois ALU entières, trois unités de calcul d'adresse, et une FPU. Le processeur incorpore, aussi un multiplieur entier, relié sur le port d'émission de la première ALU. La FPU regroupe un additionneur flottant, un multiplieur flottant, et une troisième unité LOAD/STORE pour les lectures/écritures pour les nombres flottants. L'architecture K8 ajoute une unité de manipulation de bit, la K10 un diviseur entier. [[File:Unité de calcul des processeurs AMD K7, K8 et K10.png|centre|vignette|upright=2|Unité de calcul des processeurs AMD K7, K8 et K10]] La manière d'alimenter les ALU en micro-opérations varie un petit peu entre les architectures K7, K8 et K10. Il y a cependant quelques constantes entre les trois. La première est qu'il y a une fenêtre d'instruction séparée pour les flottants, de 36 à 42 entrées, avec renommage de registre. La fenêtre d'instruction flottante a trois ports d'émission : un pour l'additionneur flottant, un autre pour le multiplieur, et un troisième pour la troisième unité flottante qui s'occupe du reste. La seconde est que chaque ALU entière est couplée avec une unité de calcul d'adresse. Par contre, la méthode de couplage varie d'un processeur à l'autre. : Les stations de réservation sont nommées des ''schedulers'' dans les schémas qui suivent. La microarchitecture K7 avait deux fenêtres d'instruction : une pour les opérations flottantes, une autre pour les instructions entières et les accès mémoire. La fenêtre d'instruction entière était reliée à 3 ALU entières et à 3 AGU. Elle pouvait émettre trois micro-opérations en même temps : trois micro-opérations entières, trois micro-opérations mémoire. Les AGU étaient reliées à la file de µops mémoire mentionnée plus haut, ce qui permet d'émettre trois µops mémoire par cycle. Par contre, la file de µops mémoire ne pouvait exécuter qu'une lecture de 64 bits ou une écriture de 64 bits. En clair, trois micro-opérations mémoire peuvent être émises par cycle, cela entraine trois calculs d'adresse simultanés, mais les trois lectures/écritures sont mises en attente dans la file de µops mémoire. Elles s'exécutent alors l'une après l'autre. La fenêtre d'instruction entière contenait 5 à 6 groupes de 3 macro-opérations. Vous noterez que j'ai parlé de macro-opérations et pas de micro-opérations, car les instructions ''load-up'' sont considérées comme une seule "micro-opération" dans la fenêtre d'instruction entière. Et cela se marie bien avec une fenêtre d'instruction unique partagée entre pipeline entier et pipeline mémoire. Une macro-opération était scindée en deux micro-opérations : une micro-opération mémoire et une micro-opération entière. Il est donc avantageux de regrouper unités mémoire et unités entières à la même fenêtre d'instruction pour ce faire. [[File:AMD K7.png|centre|vignette|upright=3|AMD K7]] Sur les architectures K8 et K10, la station de réservation unique de 15 micro-opérations est remplacée par trois stations de réservations, de 8 micro-opérations chacune pour le K8, de 10 pour le K10. Chaque station de réservation entière alimente une unité de calcul entière et une unité de calcul d'adresse. l'unité de calcul d'adresse est reliée à la file de µops mémoire, qui n’exécute toujours qu'un seul accès mémoire par cycle. Le multiplieur est relié à la première station de réservation, sur le même port d'émission que l'ALU. [[File:AMD Husky microarchitecture.png|centre|vignette|upright=3|AMD Husky microarchitecture]] La microarchitecture K10 a été déclinée en plusieurs versions, nommées Grayhound, Grayhound+ et Husky, Husky étant une architecture gravée en 32 nm dédiée aux processeurs A-3000. L'architecture Grayhound a plus de cache et un ROB plus grand, la Husky est quand à elle un peu plus différente. Elle n'a pas de cache L3, contrairement aux autres architectures K10, ce qui simplifie fortement son sous-système mémoire. Par contre, les fenêtres d'instructions/stations de réservation et le ROB sont plus grands, pareil pour les files dans l'unité mémoire. Une ALU pour les divisions entières a aussi été ajoutée. Pour résumer, les architectures K7, K8 et K10 séparent les pipelines entiers et flottants : trois pipelines entiers avec chacun son unité de calcul, et un pipeline flottant avec plusieurs unités de calcul. Les raisons à cela sont assez diverses. Disons que dupliquer des ALU entières simples prend peu de transistors, là où les gros circuits comme le multiplieur ou la FPU ne sont pas dupliqués. Et cela a un autre avantage : le renommage, ''dispatch'' et l'émission sont plus simples. Les pipelines entiers ont une exécution dans le désordre peu complexe, grâce au grand nombre d'unités de calcul, ce qui fait que le pipeline entier est de seulement 15 cycles au total (chargement et décodage inclus). A l'opposé, la FPU est alimentée par une exécution dans le désordre très complexe, avec banc de registre physique et beaucoup de ressources, mais au prix d'un pipeline flottant plus long de 3 cycles, soit 18 cycles au total. ===Les microarchitectures ZEN d'AMD=== Viennent ensuite les '''microarchitectures Bulldozer''', avec trois révisions ultérieures nommées Piledriver, Steamroller et Excavator. Mais du fait de l'utilisation de techniques de multithreading matériel que nous n'avons pas encore abordé, nous ne pouvons pas en parler ici. Les microarchitectures suivantes sont les '''architectures ZEN 1/2/3/4/5'''. Elles se ressemblent beaucoup, chacune accumulant les améliorations des précédentes. Mais le cœur de l'architecture reste plus ou moins le même. En passant à la suivante, le nombre de registre virtuel augmente, le ''branch target buffer'' augmente en taille, le ROB et les files d'attente grossissent, les caches de micro-opération aussi, les caches grossissent, etc. Une optimisation intéressante est l'ajout d'un cache de micro-opération, qui améliore grandement les performances du ''front-end'', notamment pour les boucles. La microarchitecture Zen 1 est illustrée ci-dessous. Comme on le voit, les registres flottants ont une unité de renommage séparée de celle pour les entiers, mais les deux utilisent du renommage à banc de registre physique. Il y a par contre une différence au niveau des fenêtres d'instruction, notées ''scheduler'' dans le schéma. Pour ce qui est des unités de calcul flottantes, il y a une fenêtre unifiée qui alimente quatre ALU, grâce à 4 ports d'émission. Mais pour les ALU entières, il y a une fenêtre d'instruction par ALU, avec un seul port d'émission connecté à une seule ALU. La raison de ce choix est que les opérations flottantes ont un nombre de cycle plus élevé, sans compter que les codes flottants mélangent bien additions et multiplication. Une fois décodées, les instructions sont placées dans une première file de micro-opérations om elles attendent, puis sont dispatchées soit dans le pipeline entier, soit dans le pipeline flottant. les micro-opérations entières sont insérées dans une fenêtre d'instruction directement, alors que les micro-opérations flottantes doivent patienter dans une seconde file de micro-opérations. La raison est que les micro-opérations flottantes ayant une grande latence, trop d'instructions flottantes consécutives pourraient bloquer le pipeline flottant, sa fenêtre d'instruction étant pleine. Le pipeline flottant étant bloqué, la première file de micro-opérations serait bloquée et on ne pourrait plus émettre de micro-opérations entières. Pour éviter cela, une solution serait d'agrandir la file de micro-opérations, mais cela la rendrait plus lente et se ferait donc au détriment de la fréquence d'horloge. Alors une solution a été d'ajouter une seconde file de micro-opérations, au lieu d'agrandir la première. [[File:Zen microarchitecture.svg|centre|vignette|upright=3|Microarchitecture Zen 1 d'AMD.]] Le passage à la microarchitecture n'a pas causé de grands changements. Le Zen 2 a ajouté une unité de calcul d'adresse, ce qui fait qu'on passe à 4 ALU, 3 AGU et 4 FPU. La fenêtre d'instruction flottante reste la même. Par contre, les fenêtres d'instruction entières changent un peu. Ou plutot devrais-je dire les fenêtres d'instruction mémoire. En effet, le Zen 2 fusionne les fenêtres d'instructions liées aux AGU en une seule fenêtre d'instruction deux fois plus grosse. Le Zen 5 a ajouté deux autres ALU entières et une unité de calcul d'adresse (6 ALU / 4 AGU) ==Les processeurs Atom d'Intel, de microarchitecture Bonnell== L'architecture de l'Atom première génération est assez simple. Son pipeline faisait 16 étages, ce qui est beaucoup. C'est un processeur 32 bits, ce qui aura son importance dans ce qui suit. Il était conçu pour être un processeur basse consommation, donc peu puissant. En conséquence, il n'a pas d'exécution dans le désordre, même s'il est superscalaire. C'était la norme à l'époque pour les processeurs basse consommation, que de faire sans exécution dans le désordre. De nos jours, les choses ont bien changée, même les processeurs basse consommation ont exécution dans le désordre, superscalarité et renommage de registres. ===Le ''front-end'' de l'Atom=== Le cache d'instruction permet de lire 8 octets par cycle, qui sont placés dans une file d'instruction, elle-même suivie par deux décodeurs. Le fait que les décodeurs lisent les instructions depuis une file d'instruction fait que les deux instructions décodées ne sont pas forcément consécutives en mémoire RAM. Par exemple, l'Atom peut décoder un branchement prédit comme pris, suivi par l'instruction de destination du branchement. Les deux instructions ont été chargées dans la file d'instruction et sont consécutifs dedans, alors qu'elles ne sont pas consécutives en mémoire RAM. Sur l'Atom, la majorité des instructions x86 sont décodées en une seule micro-opération, y compris les instructions ''load-up''. Le microcode n'est utilisé que pour une extrême minorité d'instructions et est à part des deux décodeurs précédents. L'avantage est que cela permet d'utiliser au mieux la file de micro-opération, qui est de petite taille. Mais surtout, cela permet de grandement réduire la consommation du processeur, au détriment de ses performances. Pour avoir un décodage rapide, malgré des instructions complexes, le processeur recourt à la technique du pré-décodage, qui prédécode les instructions lors de leur chargement dans le cache d'instruction. Le prédécodage lui-même prend deux cycles, là où une lecture dans le L1 d'instruction en prend 3. les défauts de cache d'instruction sont donc plus longs de deux cycles. Mais l'avantage du prédécodage est que la consommation d'énergie est diminuée. Prenez une instruction exécutée plusieurs fois, dans une boucle. Au lieu de décoder intégralement une instruction à chaque fois qu'on l'exécute, on la prédécode une fois, seul le reste du décodage est fait à chaque exécution. D'où un gain d'énergie assez intéressant. Les caches de micro-opération, qui sont capables d'exécuter une optimisation similaire, n'existaient pas encore à cette époque. ===Le chemin de données de l'Atom=== Les deux décodeurs alimentent une file de micro-opérations de petite taille : 32 µops maximum, 16 par ''thread'' si le ''multithreading'' matériel est activé. La file de micro-opérations a deux ports d'émission, ce qui permet d'émettre au maximum 2 µops par cycle. Les conditions pour cela sont cependant drastiques. Les deux instructions ne doivent pas avoir de dépendances de registres, à quelques exceptions près liées au registre d'état. Le multithreading matériel doit aussi être désactivé. Les deux instructions doivent aller chacun dans un port différent, et cela tient en compte du fait que les deux ports sont reliés à des unités de calcul fort différentes. Le tout est illustré ci-dessous. Les deux ports ont chacun une ALU simple dédiée, capable de faire des additions/soustractions, des opérations bit à bit et des copies entre registres. Mais ils ont aussi des opérations qui leur sont spécifiques. La séparation entre les deux pipelines est assez complexe. Il ne s'agit pas du cas simple avec un pipeline entier et un pipeline flottant séparés. En réalité, il y a deux pipelines, chacun capables de faire des opérations entières et flottantes, mais pas les mêmes opérations. Le premier port permet d’exécuter des opérations entières simples, une addition flottante, des comparaisons/branchements, ou une instruction de calcul d'adresse LEA. Le second port/pipeline est, quant à lui, conçu pour exécuter les instruction ''load-up'' nativement, en une seule micro-opération. Il contient toute la machinerie pour faire les accès mémoire, notamment des unités de calcul d'adresse et un cache L1 de données. A la suite du cache, se trouvent une ALU entière simple, un ''barrel shifter'', et un circuit multiplieur/diviseur. Le circuit multiplieur/diviseur est utilisé à la fois pour les opérations flottantes et entières. [[File:Intel Atom Microarchitecture.png|centre|vignette|upright=2.5|Intel Atom Microarchitecture]] Cette organisation difficile à comprendre est en réalité très efficace, très économe en circuit, tout en gardant une performance intéressante. Les instructions simples, ADD/SUB/bitwise sont supportées dans les deux pipelines. Il faut dire que ce sont des opérations courantes qu'il vaut mieux optimiser au mieux. Le processeur peut donc émettre deux opérations simples et fréquentes en même temps, ce qui augmente les performances. Les opérations plus complexes, à savoir les multiplications/divisions/décalages/rotations/manipulations de bit sont supportées dans un seul pipeline. La raison est qu'il est rare que de telles opérations soient consécutives, et qu'il n'est donc pas utile d'optimiser pour cette situation. Si les deux pipelines devaient supporter ces opérations, cela demanderait de dupliquer les circuits multiplieurs/diviseur, ce qui aurait un cout en circuit important pour un gain en performance assez faible. ===Le système d'exceptions flottantes de l'Atom=== Le processeur étant sans exécution dans le désordre, ses instructions doivent écrire dans les registres dans l'ordre du programme. En conséquence, certaines instructions doivent être retardées, leur émission doit attendre que les conditions soient adéquates. Et cela pose problème avec les opérations flottantes, vu qu'elles prennent pas mal de cycles pour s'exécuter. Imaginez qu'une instruction flottante de 10 cycles soit suivie par une instruction entière. En théorie, on doit retarder l'émission de l'instruction entière de 9 cycles pour éviter tout problèmes. Le cout en performance est donc assez important. En théorie, les instructions entières et flottantes écrivant dans des registres séparés, ce qui fait que l'on pourrait exécuter instructions entières et flottantes dans le désordre. Sauf pour les instructions de copie entre registres entier et flottants, mais laissons-les de côté. Le problème est qu'une instruction flottante peut parfois lever une exception, par exemple en cas de division par zéro, ou pour certains calculs précis. Si une exception est levée, alors l'instruction flottante est annulée, de même que toutes les instructions qui suivent, y compris les opérations entières. Ce n'est pas un problème si le processeur gère nativement les exceptions précises, par exemple avec un tampon de ré-ordonnancement. Mais l'Atom étant un processeur sans exécution dans le désordre, les instructions entières devraient être mises en attente tant qu'une instruction flottante est en cours d'exécution. Heureusement, l'Atom d'Intel a trouvé une parade. La technique, appelée ''Safe Instruction Recognition'' par Intel, est décrite dans le brevet US00525721.6A. L'idée est de tester les opérandes flottantes, pour détecter les combinaisons d'opérandes à problème, dont l'addition/multiplication peut lever une exception. Si des opérandes à problème sont détectées, on stoppe l'émission de nouvelles instructions en parallèle de l'instruction flottante et l'unité d'émission émet des bulles de pipeline tant que l'instruction flottante est en cours. Sinon, l'émission multiple fonctionne. La technique permet ainsi de ne pas écrire dans les registres entiers/flottants dans l'ordre du programme : une instruction entière peut être autorisée à s'exécuter même si elle écrit dans un registre entier avant qu'une instruction flottante délivre son résultat. ==La microarchitecture Netburst du Pentium 4== Dans cette section, nous allons voir l'architecture du processeur Pentium 4, qu'on a volontairement laissée de côté précédemment. Pourquoi un tel saut dans le temps ? Parce que le Pentium est complément à part des autres architectures Intel. Le Pentium 4 a représenté une rupture en termes de microarchitecture, qui a été un échec tellement retentissant que les processeurs suivants sont repartis sur la base du Pentium 3. Il introduisait de nombreuses nouveautés architecturales qui étaient très innovantes. Par exemple, il introduisait le renommage avec un banc de registre physique, qui a été utilisé sur tous les processeurs Intel suivants. Mais la plupart de ces innovations étaient en réalité de fausses bonnes idées, ou du moins des idées difficiles à exploiter. Par exemple, le système de pipeline à ''replay'' n'a été utilisé que sur le Pentium 4 et aucun autre processeur ne l'a implémenté. ===Un focus sur la fréquence d'horloge=== La microarchitecture du Pentium 4 a été déclinée en plusieurs versions, dont les finesses de gravure n'étaient pas les mêmes. La microarchitecture Netburst, utilisée sur le Pentium 4, utilisait un pipeline à 20 étage, augmenté à 32 sur une révision ultérieure. Il a existé quatre révisions de l'architecture : Willamette (180 nm), Northwood (130 nm), Prescott (90 nm) et Cedar Mill (65 nm). Un point important est que le Pentium 4 était prévu pour fonctionner à haute fréquence. Ses 1,5 GHz étaient impressionnants pour l'époque, les autres processeurs tournant à une fréquence proche du GigaHertzs. Pour cela, la solution retenue par Intel a été un pipeline très long, avec beaucoup d'étages. Le Pentium 4 a été décliné en plusieurs versions assez proches, chacune avec sa propre finesse de gravure qui n'ont pas toute le même pipeline. Les micro-architectures ''Willamette'' et ''Northwood'' avaient un pipeline de 20 étages, alors que les autres processeurs de l'époque avaient entre 10 et 15 étages maximum. Les micro-architectures ''Prescott'' et ''Cedar Mill'' étaient une refonte qui a fait grimper le nombre d'étages à 31 ! Du jamais vu, il s'agit d'un record pour un processeur commercial. Un pipeline aussi long permet d'exécuter beaucoup d’instructions en même temps, chacune dans un étage, mais aussi d'atteindre de hautes fréquences facilement. Le problème est qu'un pipeline avec autant d'étages a beaucoup de problèmes. Un point important est que la prédiction de branchement est cruciale. Pour rappel, la pénalité en cas de mauvaise prédiction dépend du nombre d'étages avant que le branchement soit résolu. Et les branchements sont résolus soit en fin de décodage, soit dans l'unité de calcul. C'est à dire au milieu du pipeline, soit en fin de pipeline. La pénalité en cas de mauvaise prédiction de branchement était énorme sur le Pentium 4, elle atteignait facilement 30 cycles Pour compenser, le Pentium 4 avait une prédiction de branchement très performante, pour l'époque. J'insiste sur le pour l'époque. Il utilisait un prédicteur qu'on a déjà abordé dans le chapitre sur la prédiction de branchement, précisément un prédicteur adaptatif à deux niveaux avec un historique global de 16 bits. Il avait aussi un ''Branch Target Buffer'' de 4096 entrées. Mais surtout, il intégrait une sorte de précurseur du cache de micro-opération, appelé le cache de traces, qui est détaillé dans la section suivante. ===Le cache de trace du Pentium 4=== Les décodeurs du Pentium 4 ne font pas décoder les instructions, ils mémorisent le résultat dans un cache de micro-opération un peu particulier, appelé le '''cache de trace'''. Une ligne de cache peut mémoriser 6 micro-opérations, ce qui peu sembler peu mais a été repris sur les micro-architectures suivantes. Mais le cache de trace a une grande différence avec un cache de micro-opération normal. Un cache de micro-opération normal mémorise une instruction par ligne de cache. Une instruction est décodée en plusieurs micro-instructions, qui sont enregistrées dans une ligne de cache. Si l'instruction n'utilise par les 6 micro-opérations disponibles, le reste de la ligne de cache n'est pas utilisé. Mais le Pentium 4 optimise le tout de manière ce à ce que ne soit pas le cas. Sur le Pentium 4, la contrainte du "une instruction par ligne de cache" est abandonnée. Une ligne de cache mémorise 6 micro-opérations consécutives, qui peuvent appartenir à plusieurs instructions. Par exemple, si le décodeur décode 4 instructions consécutives en 6 micro-opérations au total, alors le tout prendra une seule ligne de cache sur le Pentium 4. Et les 4 instructions consécutives n'ont même pas à être consécutives en mémoire : il peut y avoir des branchements pris entre ces instructions ! {|class="wikitable" |+ Cache de trace |- ! Ligne de cache | ADD || SUB || ADD || MOV || MUL || ''shift'' |- ! Ligne de cache | colspan="3" | ADD ''load-up'' || MUL || colspan="2" | Branch if Equal |- ! Ligne de cache | XOR || colspan="4" | POP || SUB |- ! ... | colspan="6" | ... |} Pour expliquer cela plus concrètement, nous allons devoir introduire les concepts de trace et de bloc de base. Un '''bloc de base''' (''basic block'') est une suite d'instructions sans branchement, qui est séparé par deux branchements. Le début d'un bloc de base est la destination d'un branchement, un bloc de base se termine avec un branchement. Une '''trace''' est formée en concaténant plusieurs blocs de base. Pour donner un exemple, regardez le code illustré ci-contre. Il est composé d'un bloc de base A, suivi par un bloc de base B, qui peut faire appel soit au bloc C, soit un bloc D. Un tel code peut donner deux traces : ABC ou ABD. La trace exécutée dépend du résultat du branchement qui choisit entre C et D. Le cache de trace mémorise des traces de 6 micro-opérations consécutives. Les traces sont formées en sortie des décodeurs d'instruction, par de subtiles opérations mélangeant mémorisation, décalage et concaténation. Les circuits qui construisent les traces ne sont pas connus, mais ils doivent certainement être très compliqués. toujours est-il qu'un cache de trace peut mémoriser des traces différentes, même si leur début est le même. Par exemple, prenons deux traces, composées des blocs de base A, B, C et D. La première trace est la trace ABC, la seconde est la trace ABD. Les deux traces auront chacune une ligne de cache dédiée. Une trace est réutilisable quand le premier bloc de base est identique et que les prédictions de branchement restent identiques. Pour vérifier cela, le tag du cache de traces contient l'adresse du premier bloc de base, la position des branchements dans la trace et le résultat des prédictions utilisées pour construire la trace. Le résultat des prédictions de branchement de la trace est stocké sous la forme d'une suite de bits : si la trace contient n branchements, le n-ième bit vaut 1 si ce branchement a été pris, et 0 sinon. Même chose pour la position des branchements dans la trace : le bit numéro n indique si la n-ième instruction de la trace est un branchement : si c'est le cas, il vaut 1, et 0 sinon. Si la trace est réutilisée par la suite, elle est lue depuis le cache de traces. Pour savoir si une trace est réutilisable, l'unité de chargement envoie le ''program counter'' au cache de traces, l'unité de prédiction de branchement fournit le reste des informations. Si on a un succès de cache de traces, et la trace est envoyée directement au décodeur. Sinon, la trace est chargée depuis le cache d'instructions et assemblée. Il faut signaler que le cache de trace avait sa propre unité de prédiction de branchement séparée de l'unité de prédiction de branchement normale. [[File:TraceCache.png|centre|vignette|upright=2|Cache de traces.]] Le cache de traces réduisait la longueur du pipeline en cas de succès de cache de trace. Quand les instructions étaient lues depuis le cache de trace, les étages avant le cache de trace ne sont pas utilisés, tout se passe comme s'ils étaient retirés du pipeline. C'est la même chose avec le cache de micro-opération des processeurs modernes, mais l'idée n'existait pas encore à l'époque. Le cache de trace mémorise des traces décodées, ce qui fait qu'un succès de cache de trace contournait non seulement le cache d'instruction, mais aussi les décodeurs. Le temps d'accès au cache de trace pouvait être assez élevé, même s'il était comparable au temps d'accès du cache d'instruction. Le cache de traces a depuis été remplacé par une alternative bien plus intéressante, le cache de micro-opérations, plus flexible et plus performant. Comparé à un cache de trace, la contrainte "une instruction par ligne de cache" simplifie grandement l'implémentation d'un cache de micro-opération. Ne parlons pas de la détection des succès de cache, qui demande d'utiliser les prédictions de branchement. Mais le vrai problème avec le cache de trace est tout autre. Il arrive souvent qu'une micro-opération soit présente dans plusieurs lignes de cache en raison du processus de construction des traces, chose impossible avec un cache de micro-opération. Et c'est un problème, qui réduit la capacité effective du cache de trace. Alors certes, une ligne de cache est plus remplie que sur un cache de micro-opération, on est certain que les 6 micro-opération par ligne de cache sont remplies. Mais la redondance réduit grandement cet avantage. ===L'exécution dans le désordre et le chemin de données du P4=== Le renommage de registres se fait avec un banc de registres physiques avec une table d'alias. Le Pentium 4 avait une exécution dans le désordre très limitée, basée sur la présence de deux files de micro-opération : une pour les accès mémoire, une autre pour les autres instructions. La seconde file regroupait opérations entières et flottantes, elles n'étaient pas séparées. Avec ces deux files, les instructions mémoire étaient exécutées dans l'ordre du programme, les instructions arithmétiques s'exécutaient aussi dans l'ordre du programme, mais une instruction arithmétique pouvait passer avant une instruction mémoire et inversement. L'avantage est que cela permettait de faire des lectures en avance, c'était une forme limitée de lecture non-bloquantes. Il s'agit bel et bien de deux files d'instructions, pas de fenêtres d'instruction ni de stations de réservation. Le Pentium 4 est le seul processeur commercial qui a utilisé des files de micro-opération séparées, tous les autres utilisent des fenêtres d'instruction : centralisées pour Intel, décentralisées pour AMD (en général). Le Pentium 2 et 3, bien qu'antérieurs, utilisait une station de réservation unique. Cela peut sembler être un retour en arrière, mais les files d'instructions sont bien plus larges : de 42 micro-opérations pour le Pentium 3, on passe à 120 micro-opérations pour le Pentium 4. Et vu la longueur du pipeline, qui fait qu'il y a plus d'instructions en vol, c'était une nécessité. Mais cela n'aurait pas été possible en utilisant des stations de réservation, pour des raisons de consommation électrique et/ou de budget en transistors, ce qui fait que passer à une file de micro-opération été la solution retenue. Pour les accès mémoire, le Pentium 4 utilisait donc une file de µops mémoire unique, couplée à une file d'écriture (non-représentée sur les schémas qui suivent). La file d'écriture du Pentium 4 était de 24 écritures maximum et gérait le ''Store-to-load forwarding''. Le processeur pouvait émettre une lecture et une écriture à chaque cycle. Il y avait un port d'émission pour les lectures et un autre pour les écritures, tous deux ayant chacun leur propre unité de calcul d'adresse. Le processeur contenait 3 ALU entières, 2 unités de calcul d'adresse et une FPU. La FPU était complétée par une unité pour faire des copies entre registres flottants, des opérations MOV. Pour les AGU, il y en avait une dédiée aux lectures, une autre pour les écritures. Le tout était relié aux ports d'émissions comme suit : [[File:Ports d'émission du Pentium 4.png|centre|vignette|upright=2.5|Ports d'émission du Pentium 4]] Le processeur utilisait deux réseaux de contournement séparés : un pour les opérations flottantes, un pour les opérations entières. Le réseau de contournement pour les opérations entières est aussi relié aux unités de calcul d'adresse. Jusque là, rien de surprenant, le chemin de données du processeur est assez classique. [[File:Architettura Pentium 4.png|centre|vignette|upright=3|Microarchitecture du Pentium 4.]] ===Les unités de calcul entières du Pentium 4=== Sur le Pentium 4, les ALU entières étaient cadencées à une fréquence double de celle du processeur. Les ALU entières pouvaient exécuter deux micro-opérations par cycle, ce qui fait que les ports d'émissions reliés aux ALU devaient eux aussi fonctionner à double fréquence. Pour faire la différence entre les deux fréquences, nous parlerons de fréquence/cycle processeur et de fréquence/cycle de l'ALU. Précisons que seules les ALU entières étaient à double fréquence, pas le multiplieur, pas le ''barrel shifter''. Pour simplifier, nous allons parler d'additionneur plutôt que de l'ALU entière, ce qui sera plus proche de la réalité. Et l'implémentation de l'additionneur du Pentium 4 était très innovante. L'additionneur pouvait exécuter deux additions par cycle, même si les deux additions ont une dépendance. Mais n'allez pas croire que l'implémentation était intuitive, avec un additionneur 32 bit basique très rapide. Non seulement l'additionneur fonctionnait à double fréquence, mais il était aussi pipeliné, avec un système de contournement interne ! Les additionneurs étaient pipelinées, d'une manière très simple. Une addition 32 bits était découpée en trois étapes : deux additions de 16 bits, une dernière étape pour mettre à jour le registre d'état. Pour cela, chaque additionneur était composé de deux additionneur 16 bits chacune, placées l'une après l'autre, avec un registre de pipeline entre les deux. L'additionneur prenait deux cycles d'horloge pour faire son travail : le premier cycle calculait les 16 bits de poids faible, le second calculait les 16 bits de poids fort lors du second cycle. Le tout est appelé '''addition étagée''' (''staggered add'') dans la documentation Intel. Une addition se fait donc en deux étapes, sauf que c'est compensé par le fait que l'additionneur fonctionnait à une fréquence double de celle du processeur ! Le résultat de ce fonctionnement franchement bizarre, est que les 16 bits de poids faible étaient calculés en une moitié de cycle processeur, alors que l'opération complète prenait un cycle. Deux additions consécutives s'exécutaient donc en 1 cycle et demi, alors qu'on aurait cru au premier abord que cela prendrait seulement un cycle. Si on fait les calculs, on s'apercoit que le rythme de croisière est cependant proche de 2 additions par cycle, bien qu'inférieur. 3 additions consécutives se font en deux cycles, 5 additions en 3 cycles, 7 en 4 cycles, etc. Et le Pentium 4 ajoutait un système de contournement interne à l'ALU. En clair, si une addition utilise le résultat de l'addition précédente, les deux peuvent s'exécuter en un cycle d'horloge et demi. Les 16 bits de poids faible de la première addition sont disponibles après un cycle ALU, ce qui permet de démarrer le calcul de la seconde addition au cycle suivant. ===Le ''replay pipeline''=== Le processeur est un processeur triple émission : il peut charger et décoder 3 µops par cycle, le ROB peut terminer 3 µops par cycle, etc. Pourtant, les ports d'émission peuvent émettre 6 instructions par cycle : 4 ports, dont deux à double fréquence. Une telle différence s'explique par l'usage d'un ''replay pipeline'', dont nous avons déjà parlé dans ce cours. Pour rappel, le Pentium 4 suppose que les lectures font tous un succès de cache L1. Si une opération arithmétique utilise la donnée lue comme opérande, le processeur l'émet immédiatement, l'opérande sera disponible une fois l'opération en entrée de l'ALU entière. Mais s'il s'est trompé, le processeur ré-exécute l'instruction après un temps d'attente de quelques cycles, pour se caler sur la latence du cache L2. Et si il y a un défaut de cache L2, l’instruction attend encore. Cela demande de ré-exécuter des instructions émises à tord, ce qui fait que le processeur doit avoir la capacité d'exécution pour. Ce pourquoi le processeur peut émettre 6 µops dans les unités de calcul : 3 µops normales et 3 µops ré-exécutées. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Les processeurs superscalaires | prevText=Les processeurs superscalaires | next=Les processeurs VLIW et EPIC | nextText=Les processeurs VLIW et EPIC }} </noinclude> eocmgzofljdurspkm0epvtiqqcpw1hg 772753 772752 2026-09-21T21:13:43Z Mewtow 31375 /* Les microarchitectures récentes d'Intel */ 772753 wikitext text/x-wiki Dans ce chapitre, nous allons étudier des exemples de processeurs x86, ceux présents dans nos PC. Nous n'allons pas voir les anciens processeurs comme le 286, le 386 ou le 486. Nous allons commencer avec le Pentium 1, et les processeurs commerciaux qui ont suivis. Tous les processeurs que nous allons voir dans ce chapitre sont des processeurs superscalaires. De fait, ce n'est pas pour rien si ce chapitre se situe après le chapitre sur les processeurs superscalaires. Par contre, nous allons voir que certains n'ont pas d'exécution dans le désordre. La raison est que l'exécution dans le désordre est arrivé après la superscalarité. ==Généralités sur les CPU x86 superscalaires== Avant de voir chaque processeur indépendamment des autres, nous allons devoir aborder quelques généralités. Nous allons d'abord voir que le jeu d'instruction x86 pose quelques problèmes pour la superscalarité. Et ces problèmes posent des contraintes assez fortes, avec lesquelles les concepteurs de processeurs dovient faire avec. Nous poursuivrons ensuite par un historique des processeurs Intel et AMD, histoire de donner un peu de contexte aux processeurs que nous allons étudier. ===Le jeu d'instruction x86 pose des problèmes pour la superscalarité=== Une difficulté de l'architecture x86 est qu'il s'agit d'une architecture CISC, avec tous les défauts que ça implique. Un jeu d'instruction CISC a en effet de nombreuses propriétés qui collent mal avec l'émission multiple, avec la '''superscalarité'''. Il y en a plusieurs, certaines impactent le chargement des instructions, d'autres leur décodage, d'autres l'exécution, etc. Premièrement, les instructions sont de longueur variable, entre 1 et 15 octets, ce qui complique leur chargement et leur décodage. En pratique, les processeurs chargent un bloc de 32 à 64 octets, et découpent celui-ci en plusieurs instructions. La conséquence est que l'usage d'instructions trop longues peut poser problème. Imaginez qu'un processeur charge un bloc de 16 octets et que celui-ci ne contienne qu'une seule instruction : on ne profite pas de la superscalarité. Deuxièmement, une partie des instructions est microcodée, faute de mieux. Et cela pose de sérieux challenges pour l'implémentation des décodeurs. Dupliquer le microcode demanderait trop de transistors, ce qui fait que ce n'est pas fait. À la place, il n'y a qu'un seul microcode, ce qui fait que l'on ne peut pas décoder plusieurs instructions microcodées en même temps. Il est cependant possible de profiter de la superscalarité, en décodant une instruction microcodée en parallèle d'autres instructions non-microcodées. Et heureusement, ce cas est de loin le plus fréquent, il est rare que plusieurs instructions microcodées se suivent. Troisièmement, la présence d'instructions ''load-up'', qui lisent un opérande en mémoire, peut poser problème, mais est aussi source d'optimisations assez intéressantes. En théorie, une instruction ''load-op'' est décodée en deux micro-opération : une pour lire d'opérande en RAM, l'autre pour faire l'opération arithmétique. Sauf que les processeurs x86 modernes optimisent la gestion des instructions ''load-up''. Par exemple, les premiers processeurs Atom géraient des micro-opérations de type ''load-up'', directement dans le chemin de données ! D'autres processeurs utilisent la technique de la '''micro-fusion''' pour retarder le décodage réel des instructions ''load-up'' assez loin dans le pipeline. Avec eux, une instruction ''load-op'' est décodée en une seule "macro-opération", qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. L'avantage est qu'une macro-opération ne prend qu'une seule entrée dans le tampon de ré-ordonnancement, la fenêtre d'instruction, la file de micro-opération, et les autres structures similaires. ===Un petit historique des processeurs x86 superscalaires=== Nous allons voir les processeurs Intel à part des processeurs AMD. La raison à cela est que les architectures Intel et AMD ont progressivement évolué, chacune se basant sur la précédente et l'améliorant. Il n'y a pas eu de cassure entre microarchitectures AMD, qui sont chacune la suite de la précédente. Il est donc préférable de voir les architectures AMD dans l'ordre chronologique. Par contre, Intel a eu une gigantesque cassure, avec le processeur Pentium 4. Son architecture se démarquait fortement du Pentium 3, mais elle n'a pas convaincu et a été abandonnée avec les processeurs suivants. Ce qui fait nous verrons l'architecture du Pentium 4 à part. Le Pentium 1 et 2 utilisaient la même architecture, qu'on détaillera dans ce qui suit. Les seules différences importantes étaient la fréquence, le cache, et quelques détails dans le genre. Le Pentium 3 était une nouvelle microarchitecture qui ajoutait l'exécution dans le désordre. Un an et demi plus tard, le Pentium 4 est sorti et a été un échec. Ses performances étaient peu convaincantes face au Pentium 3, et sa consommation énergétique était très importante. La conséquence est que le Pentium 4 et le Pentium 3 ont survécu pendant un long moment, beaucoup de monde préférait acheter un Pentium 3. Intel a alors amélioré les microarchitectures du Pentium 3 et du 4, indépendamment, pendant environ 7 ans. La microarchitecture du Pentium 3 a subit plusieurs micro-évolutions, chacune avec une finesse de gravure différente, afin de satisfaire les consomateurs. Les premeirs Pentium 3 avaient une finesse de gravure de 250 nm, elle a chuté à 65 sur les derniers modèles. L'architecture du Pentium 4 a fait la même chose, pour tenter de corriger ses problèmes de performance et de consommation d'énergie. Les premiers Pentium 4 avaient uen finesse de gravure de 180 nm, elle a elle aussi chutée à 65 sur les derniers modèles. Après l'échec du Pentium 4, les ingénieurs d'Intel ont repris l'architecture P6 et l'ont améliorée fortement, pour donner l'architecture Core. Les micro-processeurs suivants ont fait évoluer cette architecture progressivement, au point où elle ne ressemble plus à l'originale. L'architecture Core a laissé la place à l'architecture Nehalem, puis Sandy Bridge, puis Haswell, puis Skylake, puis Ice Lake, et Golden Cove. Il s'agit de la lignée principale, partant du Pentium 3 et continuant jusqu'à nos jours. Ces microarchitectures ont suivi un motif assez simple, appelé modèle '''tick-tock'''. Chaque microarchitecture était déclinée en deux versions, la seconde ayant une finesse de gravure réduite. En parallèle, Intel a travaillé sur des processeurs basse performance et basse consommation, avec une microarchitecture très différente. Les processeurs Atom de microarchitecture Bonnel, pour être ensuite remplacés par les microarchitectures Silvermont, puis Goldmont et Gracemont. Ces microarchitectures ont évolué en parallèle de la lignée principale, il s'agit d'une lignée secondaire. Le tout est résumé dans ce schéma ci-dessous. [[File:IntelProcessorRoadmap-4v.svg|centre|vignette|upright=2.5|Roadmap des processeurs Intel, qui servira de structure pour la suite du chapitre]] De nos jours, les processeurs Intel utilisent les deux types de micro-architectures en même temps. Les CPU Intel modernes disposent de deux types de coeurs : les coeurs P et le coeurs E. Leur nom signifie "Performance" et "Efficient", qui trahissent leur but. Les coeurs P utilisent la microarchitecture de la lignée principale, qui est conçue pour la performance. Les coeurs E, quant à eux, utilisent les micro-architectures basse consommation de la lignée secondaire. ==Les processeurs x86 d'Intel, la lignée principale== Pour commencer, nous allons voir les processeurs Intel. N'y voyez pas du favoritisme derrière ce choix, la justification est toute autre. Si je commence par Intel, c'est pour commencer avec les Pentium 1 et 2, qui sont plus simples que tous les autres. Il s'agit en effet de processeurs superscalaires, mais sans exécution dans le désordre. L’absence d'exécution dans le désordre les rend bien plus simples à étudier que les autres, ce qui en fait un bon point de départ pour ce chapitre. Le successeur du Pentium 2 a intégré l'exécution dans le désordre, ce qui fait que les Pentium 1 et 2 sont les seuls processeurs superscalaires ''in-order''. Pour la concurrence, AMD n'a pas produit de processeurs ''in-order'', tous les processeurs produits par AMD intègrent l'exécution dans le désordre. A la rigueur, il y a bien les processeurs Atom de la lignée secondaire d'Intel. Cependant, même s'ils sont bien des CPU ''in-order'', leur architecture est assez compliquée. En comparaison, le Pentium est une vieille architecture, qui se débrouillait avec peu de transistors et était donc bien plus simple que celle de l'Atom. ===Le Pentium 1/MMX et les pipelines U/V=== Le processeur Pentium d'Intel avait un pipeline de 5 étages : un étage de chargement/prédiction de branchement, deux étages de décodage, un étage d'exécution et un dernier étage pour l'écriture dans les registres. Le Pentium 1 était un processeur double émission, intégrant deux pipelines nommés U et V. Chose importante, les deux pipelines n'étaient pas identiques. Le pipeline U pouvait exécuter toutes les instructions, mais le pipeline V était beaucoup plus limité. Par exemple, seul le pipeline U peut faire des calculs flottants, le pipeline V ne fait que des calculs entiers et des branchements. Les deux pipelines disposaient d'une unité de calcul entière, identique dans les deux pipelines. Mais le pipeline U incorporait un circuit multiplieur/diviseur et d'un ''barrel shifter''. L'unité flottante était sur le port d'émission du pipeline U, idem pour l'unité de calcul vectoriel MMX sur le Pentium MMX. Les deux pipelines avaient chacun une unité de calcul d'adresse, mais ils n'étaient pas identiques : celle du pipeline V ne gérait que l’instruction LEA, celle du pipeline U gérait tous les calculs d'adresse. {|class="wikitable" |- ! Pipeline U ! Pipeline V |- | ALU entière | ALU entière |- | Multiplieur/diviseur | |- | ''Barrel Shifter'' | |- | AGU complexe | AGU simple (opération LEA) |- | FPU | |- | Unité SIMD | |} Les deux pipelines géraient les opérations bit à bit, les additions, les soustractions et les comparaisons. Les autres instructions ne sont exécutables que dans le pipeline U. Pour être plus précis, les deux pipelines supportaient les instructions suivantes, ce qui fait qu'on pouvait en faire deux en même temps : * Les instructions arithmétiques INC, DEC, ADD, SUB ; * l'instruction de comparaison CMP ; * les instructions bit à bit AND, OR, XOR ; * l'instruction de calcul d'adresse LEA ; * l'instruction MOV (dépend du mode d'adressage) ; * les instructions de gestion de la pile PUSH et POP (dépend du mode d'adressage) ; * l'instruction NOP, qui ne fait rien. Il faut noter qu'il y a cependant quelques restrictions, beaucoup de paires d'instructions sont interdites. La plupart interdisent au pipeline V de faire quoique ce soit quand une opération particulière est émise dans le pipeline U. Par exemple, si le pipeline U exécute une multiplication ou une division, le processeur ne peut pas exécuter une opération dans le pipeline V. Et c'est pareil avec les branchements : si un branchement est émis dans le pipeline U, l'instruction suivant le branchement n'est pas émise dans le pipeline V, pour éliminer les dépendances de contrôle. De même, si le pipeline U exécute une opération flottante, le pipeline V ne pourra rien exécuter. La seule exception est l'instruction FCXH, qui échange deux registres flottants. [[File:Intel Pentium arch.svg|centre|vignette|upright=2.5|Microarchitecture de l'Intel Pentium MMX. On voit que certaines unités de calcul sont dupliquées.]] Un choix assez intéressant a été fait pour le cache de données. Nous avions vu dans le chapitre sur les CPU superscalaires que la superscalarité a un impact sur l'unité mémoire. Il y a alors deux implémentations. La première ne fait rien, l'unité mémoire ne change pas, et le processeur ne peut pas faire deux accès mémoire simultanés. La seconde duplique l'unité mémoire et les ports de lecture/écriture du cache, ce qui autorise des accès mémoire simultanés. Le Pentium 1 utilise une solution intermédiaire. Les ingénieurs d'Intel étaient partis à la base sur un cache totalement double port, pour obtenir des performances maximales. Mais diverses simulations et observations les ont fait changer d'avis. Les simulations ont montré qu'il est "rare" que les deux pipelines aient besoin de lire/écrire dans le cache en même temps. Et ils ont optimisé le cache de donnée pour en tenir compte. Le cache de données est partiellement multiport : simple port sur certains aspects, double port sur d'autres. Le cache est un cache splité, à savoir que les données et les ''tags'' sont séparés dans des mémoires séparées. La mémoire pour les ''tags'' est multiport, ce qui permet d'interroger les ''tags'' du cache deux fois par cycle. Un port est relié au pipeline u, un autre au pipeline V. Mais pour les données, le cache n'a qu'un seul port pour lire/écrire des données. Impossible donc de lire deux données en même temps, pour alimenter les deux pipelines. Il utilise cependant 8 banques permet d'accélérer des accès mémoire proches dans le temps, mais dans des cycles d'horloge différents. Entre les deux mémoires, il y a un circuit qui détecte les conflits, à savoir les situations où les deux pipelines accèdent en même temps au cache. S'ils veulent lire/écrire une donnée dans le même cycle, ce qui est impossible avec un seul port, le pipeline U a la priorité et le pipeline V attend le cycle suivant. Le circuit détecte aussi les conflits de banque, à savoir quand un pipeline accède à une banque en cours d'accès par l'autre pipeline (on rappelle qu'un accès au cache prend plusieurs cycles). Le circuit détecte aussi certaines dépendances mémoires, à savoir des accès consécutifs à la même adresse. : La TLB du processeur est aussi totalement double port. ===La microarchitecture P6 du Pentium 2/3=== Le Pentium 3 utilisait la '''microarchitecture P6''', qui a été dérivée dans de nombreuses variantes, dont les finesses de gravure n'étaient pas les mêmes. Il introduit une exécution dans le désordre simple, avec une fenêtre d'instruction centralisée, avec renommage dans le désordre dans le ROB (tampon de ré-ordonnancement), commandé par une table d'alias. C'était un processeur triple émission, soit une instruction de plus que la double émission du Pentium 1. Le pipeline passe de 5 étage sur le Pentium à 14 - 12 étages, dont le détail est le suivant : * Prédiction de branchement, deux cycles ; * Chargement des instructions, trois cycles ; * Décodage de l'instruction, deux cycles ; * Renommage de registre, un cycle ; * Copie des opérandes dans le tampon de ré-ordonnancement (lié au renommage de registre dans le ROB) ; * Dispath dans ou depuis la station de réservation. * Exécution de l'instruction ; * Écriture du résultat dans le ROB ; * Écriture dans le banc de registre physique. Les instructions sont chargées par blocs de 16 octets, avec un système de fusion de blocs pour gérer les instructions à cheval sur deux blocs. Lors d'un branchement, deux blocs doivent être chargés si l'instruction de destination n'est pas alignée sur 16 octets et cela cause un délai de un cycle d'horloge. Le décodage des instructions x86 était géré par plusieurs décodeurs. Il y avait trois décodeurs : deux décodeurs simples, et un décodeur complexe. Les décodeurs simples décodaient les instructions les plus fréquentes, mais aussi les plus simples, qui étaient décodées en une seule micro-opération. Les instructions CISC complexes étaient gérées uniquement par le décodeur complexe, basé sur un microcode, qui pouvait fournir jusqu'à 4 micro-opérations par cycle. Le tout est résumé avec la règle 4-1-1. La toute première instruction chargée depuis la file d'instruction va dans le premier décodeur simple. Si jamais le décodeur ne peut pas décoder l'instruction, l'instruction est redirigée dans un autre décodeur, avec un délai d'un cycle d'horloge. Les stations de réservations étaient regroupées dans une structure centralisée, en sortie de l'unité de renommage. Elles avaient 5 ports d'émission, qui étaient sous-utilisés en pratique. Niveau ALU, on trouve deux ALUs entières, une flottante, une unité pour les instructions SSE et autres, et trois unités pour les accès mémoire (regroupées en une seule unité dans le schéma ci-dessous). Les unités mémoire regroupent une unité de calcul d'adresse pour les lectures, une autre pour les écritures, et une unité pour la gestion des données à écrire. Les unités de calcul d'adresse sont des additionneurs à 4 opérandes, complétement différents des ALU entières. Les ALU entières sont deux unités asymétriques : une ALU simple, et une ALU complexe incorporant un multiplieur. Les deux peuvent exécuter des opérations d'addition, soustraction, comparaison, etc. [[File:P6 func diag.png|centre|vignette|upright=2|P6 func diag]] Les premiers Pentium 3 n'avaient pas de cache L2 dans le processeur, celui-ci était sur la carte mère. Mais il a été intégré dans le processeur sur la seconde version du Pentium 3, la version Coppermine. Le Pentium 3 a servi de base aux microarchitectures d'Intel qui ont suivi. Les changements à chaque nouvelle génération sont assez mineurs : la prédiction de branchement est améliorée, la taille des stations de réservation et du ROB augmente, idem avec les autres structures liées à l'exécution dans le désordre. Les processeurs Intel ont conservé une fenêtre d'instruction centralisée, alors qu'AMD utilise une autre méthode, comme nous allons le voir dans ce qui suit. Les seuls changements notables sont est le passage à un renommage dans le ROB à un renommage à banc de registre physique, ainsi que l'introduction du cache de micro-opération. Et ce sont des modifications qu'AMD a aussi faites, celle-ci étant clairement une bonne idée pour toutes les micro-architectures avec un budget en transistor suffisant. Il est intéressant de garder cela en tête, car une bonne partie des améliorations de chaque micro-architecture proviendra de là. ===La microarchitecture Core=== La '''microarchitecture Core''' fait suite au Pentium 4, mais reprend en fait beaucoup d’éléments du Pentium 2 et 3. Elle utilise la station de réservation unique avec renommage dans le ROB, provenant du Pentium 2/3. Elle supporte aussi les optimisations des opérations ''load-up'', avec notamment un support des macro-opérations mentionnées plus haut. Les améliorations sont assez diverses, mais aussi assez mineures. * Le processeur incorpore un cache L2, en plus des caches L1 déjà présents auparavant. * La prédiction de branchement a été améliorée avec notamment l'ajout d'une ''Fetch Input Queue''. * L'architecture Core passe à la quadruple émission, soit une instruction de plus que sur le Pentium 2 et 3. Pour cela, un quatrième décodeur est ajouté, il s'agit d'un décodeur simple qui ne fournit qu'une seule micro-opération en sortie. * Un ''stack engine'' et un ''Loop Stream Detector'' ont été ajoutés, ainsi que le support de la macro-fusion qui fusionne une instruction de test et le branchement qui suit en une seule micro-opération. * Les techniques de désambiguïsation mémoire sont implémentées sur cette micro-architecture. Il y a quelques modifications au niveau de l'unité de chargement. La file d'instruction a toujours ce système de fusion de blocs, sauf que les branchements ne causent plus de délai d'un cycle lors du chargement. La file d'instruction est suivie par un circuit de prédécodage qui détermine la taille des instructions et leurs frontières, avant de mémoriser le tout dans une file de 40 instructions. La station de réservation dispose de 6 ports d'émission, mais on devrait plutôt dire 5. Sur les 5, il y en a un pour les lectures, un pour les écritures. Les deux sont reliées à une ''Load/Store Queue'', appelée ''Memory Ordering Buffer''. Elle est elle-même reliée au cache de données par deux ports : un port de lecture et un port d'écriture. Les trois ports d'émission restants sont connectés aux unités de calcul. Les trois ports récents sont chacun reliés à une ALU entière. L'additionneur flottant est connecté au port 1, alors que le multiplieur/diviseur flottante est connecté au port 0. Le fait de mettre les deux sur des ports différents permet d'émettre une addition et une multiplication flottant simultanément. Le multiplieur entier est relié au second port d'émission, celui sur lequel se trouve l'additionneur flottant. Le résultat que le processeur peut émettre un mix d'opérations flottantes et entière assez varié. [[Image:Intel Core2 arch.svg|centre|vignette|upright=2|Intel Core microarchitecture]] ===Les microarchitectures Sandy Bridge and Ivy Bridge=== Les micro-architectures suivant la micro-architecture Core ont introduit quelques grandes modifications : le passage à un renommage à banc de registre physique, l'ajout d'un cache de micro-opérations (et d'un ''Loop Stream Detector''). L'ajout du cache de micro-opérations est un gros changement, particulièrement avec le jeu d’instruction x86. Le décodage des instructions est lent, couteux en énergie. Mais avec l'introduction du cache de micro-opération, la majorité des micro-opérations est non pas décodée, mais lue depuis le cache de micro-opérations. Les décodeurs décodent les instructions pas encore exécutées, mais les exécutions suivantes sont lues depuis le cache de micro-opérations. Et vu la grande présence de boucles, le cache de micro-opérations est l'alimentation principale du pipeline. Les décodeurs servent surtout à alimenter le cache de micro-opérations, parfois décoder quelques instructions isolées exécutées de-dehors de boucles, pas plus. Concrètement, ils servent pour 10 à 20% des micro-opérations exécutées. Intel a d'ailleurs reflété ce fait dans sa terminologie. Intel distingue deux voies de chargement : le ''legacy pipeline'' et le cache de micro-opérations. L'unité de chargement et les décodeurs sont regroupés dans la voie du ''legacy pipeline''. Le cache de micro-opérations est complété avec un ''Loop Stream Detector'', placé après le cache en question. Les décodeurs et le cache de micro-opérations alimentent une file de micro-opérations, située juste avant l'étage de renommage de registres. La file de micro-opérations sert en quelque sorte de tampon entre l'étage de "décodage" et celui de renommage. Le ''Loop Stream Detector'' utilise cette file de micro-opérations comme d'un cache lorsqu'une boucle est détectée. Les micro-opérations de la boucle sont lue depuis la file de micro-opérations, pour être envoyée au renommeur de registres. L'avantage est que le cache de micro-opérations et/ou les décodeurs sont mis en pause et clock-gatés lorsqu'une boucle s'exécute, ce qui réduit la consommation du processeur. Le ''Loop Stream Detector'' et le cache de micro-opération ont globalement le même effet : désactiver tout ce qui est avant, le ''Loop Stream Detector'' appliquant cette méthode au cache de micro-opération lui-même.. Voyons maintenant quelles sont les micro-architectures qui implémentent ces optimisations. Les microarchitectures '''Sandy Bridge''' and '''Ivy Bridge''' sont similaires à l'architecture Core, si ce n'est pour le passage à un renommage à banc de registre physique, et l'ajout d'un cache de micro-opérations. Le nombre de ports d'émission passe à 7, avec 4 pour les instructions arithmétiques (flottantes comme entière), 2 pour les lectures, et un pour les écritures (en fait deux, avec un pour le calcul d'adresse, l'autre pour la donnée à écrire). Pour le reste, rien ne change si ce n'est la prédiction de branchement Les architectures '''Haswell''' et '''Broadwell''' ont ajouté quelques unités de calcul, élargit la sortie du cache de micro-opérations. Un port d'émission pour opération entières a été ajouté, de même qu'un port pour les accès mémoire. Le processeur passe donc à 8 ports d'émission, ce qui permet d'émettre jusqu'à 8 micro-opérations, à condition que le cache de micro-opération suive. Pour le reste, le processeur est similaire aux architectures précédentes, si ce n'est que certaines structures grossissent. L'architecture '''Skylake''' réorganise les unités de calcul et les ports d'émission pour gagner en efficacité. Pour le reste, les améliorations sont mineures. A la rigueur, l'unité de renommage de registre ajoute des optimisations comme l'élimination des MOV, les idiomes liés aux opérations avec zéro, etc. ===Les microarchitectures récentes d'Intel=== Les architectures '''Ice Lake''' et '''Tiger Lake''' passent de quadruple émission à la pentuple émission. Par contre, le processeur utilise toujours 4 décodeurs. Mais les micro-opérations étant émises depuis le cache de micro-opérations, ce n'est pas un problème pour la pentuple émission. Le processeur peut parfaitement émettre 5 micro-opérations en même temps, si elles sont lues depuis le cache de micro-opérations. Là encore, on voit à quel point le cache de micro-opération découple ce qu'il y avant de ce qu'il y a après. La microarchitecture '''Golden Cove''' altère les décodeurs et l'unité de chargement. Sur toutes les générations précédentes, on reste sur une unité de chargement qui charge 16 octets à la fois et il y a toujours 4 décodeurs identiques aux générations précédentes. Golden Cove passe à 6 décodeurs simples, et double la taille du chargement qui passe à 32 octets. Une telle stagnation sur les unités de chargement et de décodage s'explique encore une fois par la présence du cache de micro-opération fait que ce n'est pas trop un problème. Tout ce qui précède le cache de micro-opérations n'a pas de raison d'évoluer, car ce cache est très puissant. Niveau unités de calcul, le CPU a pas moins de 5 ALU entières, deux ''barrel shifters'', un multiplieur et deux unités de branchements. Le tout est répartit sur 5 ports d'émission. Pour les unités mémoire, il y a trois unités LOAD pour les lectures et deux unités STORE pour les écritures. Il n'y a plus d'unité flottante proprement dite, mais une unité SIMD qui est capable de faire plusieurs calculs flottants, qu'on ne détaillera pas ici, car nous n'avons pas encore vu les techniques de SIMD. Notons qu'il y a trois fenêtres d'instruction séparées pour : les instructions entières/flottantes, les lectures, les écritures. [[File:Golden Cove.png|centre|vignette|upright=3|Golden Cove]] ==Un étude des microarchitectures superscalaires x86 d'AMD== Les architectures Intel ont évolué progressivement, sans grandes cassure. Il y a une continuité presque initerrompue entre l'architecture du Pentium 2 et les architectures modernes. Intel a fait des améliorations mineures à chaque nouvelle micro-architecture, si on omet le passage à un renommage à banc de registre physique et l'ajout du cache de micro-opération. A l'opposé, les architectures AMD ont eu de nombreuses cassures dans la continuité où AMD a revu sa copie de fond en comble. Étudier ces architectures demande de voir trois choses séparément : le ''front-end'' qui regroupe l'unité de chargement et les décodeurs, le ''back-end'' qui gère l'exécution dans le désordre et les unités de calcul, et le sous-système mémoire avec les caches et la ''Load Store Queue''. Leur étude sera plus ou moins séparée dans ce qui suit, pour chaque classe d'architecture. ===La première génération de CPU AMD : les architectures K5, K6, K7, K8 et K10=== La première génération de processeurs AMD est celle des architectures K5, K6, K7, K8 et K10. Il n'y a pas de K9, qui a été abandonné en cours de développement. Les processeurs K5 et K6 portent ce nom au niveau commercial. Par contre, les processeurs d'architecture K7 sont aussi connus sous le nom d''''AMD Athlon''', les AMD K8 sont connus sous le nom d''''AMD Athlon 64''', et les architecture K10 sont appelées les '''AMD Phenom'''. Comme le nom l'indique, l'architecture K8 a introduit le 64 bits chez les processeurs AMD. Elles ont une architecture assez similaire pour ce qui est du chargement et des caches. Toutes disposent d'au minimum un cache L1 d'instruction et d'un cache L1 de données. Le K5 n'avait que ces caches, mais un cache L2 a été ajouté avec le K7, puis un L3 avec le K10. L'AMD K5 avait une TLB unique, mais les processeurs suivants avaient une TLB pour le L1 d'instruction et une autre pour le L1 de données. Idem pour le cache L2, avec deux TLB : une pour les données, une pour les instructions. Les caches L1/L2 sont de type exclusifs, à savoir que les données dans le L1 ne sont pas recopiées dans le L2. Le cache L2 est précisément un cache de victime, qui mémorise les données/instructions, évincées des caches L1 lors du remplacement des lignes de cache. L'introduction du cache L2 a entrainé l'ajout de deux TLB de second niveau : une L2 TLB pour les données et une autre pour les instructions. Les architectures K8 et K10 ont ajouté un cache L3, avec un accès indirect à travers l'interface avec le bus. : L'AMD K7 originel, aussi appelée Athlon classique, n'avait pas de cache L2, mais celui-ci était placé sur la carte mère et fonctionnait à une fréquence moitié moindre de celle du CPU. L'Athlon Thunderbird, puis l'Athlon XP, ont intégré le cache L2 dans le processeur. {|class="wikitable" |- ! Architecture AMD ! colspan="5" | Caches |- | rowspan="2" | K5 | L1 instruction || L1 données || colspan="3" | |- | colspan="2" | TLB unique || colspan="3" | |- | colspan="4" | |- | rowspan="2" | K6 | L1 instruction || L1 données || colspan="3" | L2 unifié |- | TLB L1 instruction || TLB L1 données || colspan="3" | |- | colspan="6" | |- | rowspan="2" | K7, K8 | L1 instruction || L1 données || colspan="2" | L2 unifié || |- | TLB L1 instruction || TLB L1 données || TLB L2 instruction || TLB L2 données || |- | colspan="6" | |- | rowspan="2" | K10 | L1 instruction || L1 données || colspan="2" | L2 unifié || L3 |- | TLB L1 instruction || TLB L1 données || TLB L2 instruction || TLB L2 données || |} Fait important, les architectures K5 à K10 utilisent la technique du '''prédécodage''', où les instructions sont partiellement décodées avant d'entrer dans le cache d'instruction. Le prédécodage facilite grandement le travail des décodeurs d'instruction proprement dit. Par contre, le prédécodage prend de la place dans le cache L1 d'instruction, une partie de sa capacité est utilisé pour mémoriser les informations prédécodées. C'est donc un compromis entre taille du cache et taille/rapidité des décodeurs d'instruction. Sur les architectures K5 et K6, le prédécodage précise, pour chaque octet, si c'est le début ou la fin d'une instruction, si c'est un octet d'opcode, en combien de micro-opérations sera décodée l'instruction, etc. A partir de l'AMD K7, le prédécodage reconnait les branchements inconditionnels. Lorsqu'un branchement inconditionnel est pré-décodé, le pré-décodage tient compte du branchement et continue le pré-décodage des instructions à partir de la destination du branchement. Le système de prédécodage est abandonnée à partir de l'architecture Bulldozer, qui suit l'architecture K10. La prédiction de branchement de ces CPU tire partie de ce système de pré-décodage, à savoir que les prédictions de branchement sont partiellement mémorisées dans les lignes de cache du L1 d'instruction. Par exemple, l'AMD K5 se passe de ''Branch Target Buffer'' grâce à cela. Si une ligne de cache contient un branchement, elle mémorise l'adresse de destination de ce branchement, en plus des bits de pré-décodage. Si il y a plusieurs branchements dans une ligne de cache, c'est l'adresse de destination du premier branchement pris dans cette ligne de cache qui est mémoirsée. Un défaut de cette approche est que si le branchement n'est pas dans le L1 d'instruction, aucune prédiction de branchement ne peut être faite et le préchargement ne peut pas fonctionner. C'est une limitation que n'ont pas les BTB découplées du cache L1 : elles peuvent prédire un branchement qui a été évincé dans le L2 ou le L3, tant que l'entrée associée est dans le BTB. Les prédictions peuvent même servir à précharger les instructions utiles. [[File:Comparaison du chargement de l'AMD K5 et K6.png|centre|vignette|upright=2|Comparaison du chargement de l'AMD K5 et K6]] Au niveau du décodage, on trouve de nombreuses différences entre les premières architectures AMD. L'AMD K5 contient 4 décodeurs hybrides, afin de décoder 4 instructions par cycles. Le K5 a quatre décodeurs simples couplés à 4 décodeurs complexes avec chacun un accès au micro-code. Une instruction peut donc passer par a donc deux voies de décodage : un décodage rapide et simple pour les instructions simples, un décodage lent et passant par le microcode pour les instructions complexes. Pour décoder 4 instructions, les deux voies sont dupliquées en 4 exemplaires, ce qui a un cout en circuits non-négligeable. L'AMD K6 utilise moins de décodeurs et ne peut que décoder deux instructions à la fois maximum. Par contre, il fournit en sortie 4 micro-opérations. Il intègre pour cela deux décodeurs simples, un décodeur complexe et un décodeur micro-codé. Un décodeur simple transforme une instruction simple en une ou deux micro-opérations. Il est possible d'utiliser les deux décodeurs simples en même temps, afin de fournir 4 micro-opérations en sortie du décodeur. Les deux autres décodent une instruction complexe en 1 à 4 micro-opérations. Si jamais la ou les deux instructions sont décodées en 1, 2 ou 3 micro-opérations, les micro-opérations manquantes pour atteindre 4 sont remplies par des NOPs. Pour le K7 et au-delà, le processeur dispose de décodeurs séparées pour les instructions micro-codées de celles qui ne le sont pas. Le processeur peut décoder jusqu’à 3 instructions par cycle. Le décodage d'une instruction microcodée ne peut pas se faire en parallèle du décodage non-microcodé. C'est soit le décodeur microcodé qui est utilisé, soit les décodeurs câblés, pas les deux en même temps. Le décodage d'une instruction prend 4 cycles. Les instructions non-microcodées sont décodées en une seule micro-opération, à un détail près : le CPU optimise la prise en charge des instructions ''load-up''. La différence entre le K6 et le K7 s'explique par des optimisations des instructions ''load-up''. Sur le K6, les instructions ''load-up'' sont décodées en deux micro-opération : la lecture en RAM, l'opération proprement dite. Mais sur le K7, une instruction ''load-up'' est décodée en une seule micro-opération. En conséquence, les décodeurs simples sont fortement simplifiés et le décodeur complexe disparait au profit d'un microcode unique. [[File:Décodage sur le K5 et le K5.png|centre|vignette|upright=3|Décodage sur le K5 et le K5]] ====Les microarchitectures K5 et K6 d'AMD==== Les deux premières architectures étaient les architectures K5 et K6, l'architecture K6 ayant été déclinée en quatre versions, nommées K6-1, K6-2, et K-3, avec une version K6-3 bis. Elles sont regroupées ensemble car elles ont beaucoup de points communs. Par exemple, tout ce qui a trait au chargement et au cache était similaire, de même que les unités de calcul. Les deux architectures avaient n'avaient pas de cache L2 et devaient se contenter d'un cache L1 d'instruction et d'un cache L1 de données. L'AMD K5 incorpore une TLB unique, alors que le K6 utilise des TLB séparées pour le cache d'instruction et le cache de données. Une différence entre l'architecture K5 et K6 est que la première utilise des caches normaux, alors que la seconde utilise des ''sector caches''. Les deux architectures disposaient des unités de calcul suivantes : deux ALU entières, une FPU, deux unités LOAD/STORE pour les accès mémoire, une unité de branchement et une ou plusieurs unités SIMD. Une organisation classique, donc. Pour les unités entières, il y avait deux ALU simples, un ''barrel shifter'' et un diviseur. Il n'y a pas d'erreur, le processeur incorpore un circuit diviseur, mais pas de circuit multiplieur. La raison est que la multiplication est réalisée par la FPU ! En effet, le multiplieur flottant de la FPU intègre un multiplieur entier pour multiplier les mantisses, qui est utilisé pour les multiplications entières. La même technique a été utilisée sur l'Atom, comme vu plus haut. Le tout était alimenté par deux ports d'émission, appelés ports X et Y. Sur l'architecture K5, le ''barrel shifter'' et le diviseur sont des ports différents. {|class="wikitable" |+ AMD K5 |- ! Port X ! Port Y |- | ALU simple | ALU simple |- | ''Barrel Shifter'' | Diviseur |} Sur l'architecture K6, le ''barrel shifter'' et le diviseur sont sur le même port. {|class="wikitable" |+ AMD K6 |- ! Port X ! Port Y |- | ALU simple | ALU simple |- | | ''Barrel Shifter'' |- | | Diviseur |} Niveau unités mémoire, le K5 avait deux unités LOAD/STORE, chacune capable de faire lecture et écriture. Par contre, la ''store queue'' n'a qu'un seul port d'entrée, ce qui fait que le processeur peut seulement accepter une écriture par cycle. Le processeur peut donc émettre soit deux lectures simultanées, soit une lecture accompagnée d'une écriture. Impossible d'émettre deux écritures simultanées, ce qui est de toute façon très rare. L'architecture K6 utilise quant à elle une unité LOAD pour les lectures et une unité STORE pour les écritures. Ce qui permet de faire une lecture et une écriture par cycle, pas autre chose. Niveau unités SIMD, l'architecture K7 n'avait qu'une seule unité SIMD, placée sur le port d'émission X. L'architecture K8 ajouta une seconde unité SIMD, sur l'autre port d'émission entier. De plus, trois ALU SIMD ont été ajoutées : un décaleur MMX, une unité 3DNow!, une unité mixte MMX/3DNow. Elles sont reliées aux deux ports d'émission entier X et Y ! Elles ne sont pas représentées ci-dessous, par souci de simplicité. [[File:Unité de calcul des processeurs AMD K5 et K6.png|centre|vignette|upright=2|Unité de calcul des processeurs AMD K5 et K6. les unités sur la même colonnes sont reliées au même port d'émission.]] Si les unités de calcul et le chargement sont globalement les mêmes, les deux architectures se différencient sur l'exécution dans le désordre. L'AMD K5 utilise du renommage de registre dans le ROB avec des stations de réservation. Par contre, l'AMD K6 utilise une fenêtre d'instruction centralisée. De plus, son renommage de registre se fait avec un banc de registre physique. L'architecture AMD K5 utilisait de deux stations de réservation par unité de calcul, sauf pour les deux unités mémoire partageaient une station de réservation unique (deux fois plus grande). Les stations de réservation sont cependant mal nommées, vu que ce sont en réalité des mémoire FIFO. Une micro-opération n'est émise que si elle est la plus ancienne dans la FIFO/station de réservation. Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique. Le tampon de ré-ordonnancement faisait seulement 16 instructions. [[File:AMD K5.jpg|centre|vignette|upright=3|AMDK5 Diagramme.]] L'architecture K6 remplace les stations de réservations par une fenêtre d'instruction centralisée. Les 4 micro-opérations renommées sont écrites dans la fenêtre d'instruction par groupe de 4, NOP de ''padding'' inclus. La fenêtre d'instruction centralisé contient 24 micro-opérations, groupées en 6 groupes de 4 micro-opérations, avec potentiellement des NOP dedans suivant le résultat du décodage. L'avantage est que l'implémentation de la fenêtre d'instruction est simple. La fenêtre d'instruction centralisée permettait d'émettre 6 micro-opérations en même temps (une par unité de calcul/mémoire). Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique. Le processeur utilisait un renommage avec un banc de registre physique. Le banc de registre physique pour les entiers contenait 48 registres, dont 24 étaient des registres architecturaux et 24 étaient des registres renommés. Sur les 24 registres architecturaux, 16 avaient une fonction de ''scratchpad'' que les ''datasheets'' d'AMD ne détaillent pas, les 8 restants étaient les registres généraux EAX, EBX, etc. [[File:AMD K6 Little foot & Modl 6.png|centre|vignette|upright=3|AMD K6 original.]] ====Les microarchitectures K7, K8 et K10 d'AMD==== Les microarchitectures suivantes sont les architectures K7, K8 et K10. Les architectures K7, K8 et K10 sont assez similaires. La différence principale entre le K7 et le K8 est le support du 64 bits. Les apports du K10 sont la présence d'un cache L3, d'une unité de calcul supplémentaire et d'améliorations de la prédiction de branchement. La taille de certains caches a été augmentée, de même que la largeur de certaines interconnexions/bus. L'architecture K7 des processeurs Athlon utilisait le renommage de registre, mais seulement pour les registres flottants, pas pour les registres entiers. Le ranommeg des registres flottants étaient réalisé via un banc de registres physique, ne contenant que des registres flottants. Les architectures K8 et K10 utilisent le renommage de registres pour tous les registres, entiers comme flottants. Par contre, le renommage de registre n'est pas réalisé de la même manière pour les registres entiers et flottants. Les registres entiers sont renommés dans le tampon de ré-ordonnancement, comme c'était le cas sur les architectures Intel avant le Pentium 4. Par contre, les registres flottants sont renommés grâce à un banc de registre physique. Le K8 est donc un processeur au renommage hybride, qui utilise les deux solutions de renommage principales. A partir du K7, le CPU optimise la prise en charge des instructions ''load-up''. Les instructions ''load-op'' sont appelées des macro-opérations dans la terminologie d'AMD, et aussi d'Intel. L'idée est que les instructions ''load-up'' sont décodées en micro-opérations intermédiaires. Elles sont propagées dans le pipeline comme étant une seule micro-opération, jusqu'à l'étage d'émission. Lors de l'émission, les instructions ''load-up'' sont scindées en deux micro-opérations : la lecture de l'opérande, puis l'opération proprement dite. Faire ainsi économise des ressources et optimise le remplissage du tampon de ré-ordonnancement, des fenêtres d'instructions, des stations de réservation, etc. Le tampon de réordonnancement est combiné avec divers circuits en charge de l'exécution dans le désordre, dans ce qui s'appelle l'''instruction control unit''. Il contient de 72 à, 84 instructions, qui sont regroupées en groupes de 3. Là encore, comme pour le K5 et le K6, le tampon de réordonnancement tient compte de la sortie des décodeurs. Les décodeurs fournissent toujours trois micro-opérations par cycle, quitte à remplir les vides par des NOP. Le tampon de réordonnancement reçoit les micro-opérations, NOP inclus, par groupes de 3, et est structuré autour de ces triplets de micro-opération, y compris en interne. Pour ce qui est de l'unité mémoire, elle est précédée par une file de µops mémoire, qui émet les accès mémoire dans l'ordre du programme. Elle est souvent qualifiée de ''Load-Store Queue'', mais ce n'est pas la terminologie que nous utilisons dans ce cours. La file de micro-opération lire/écrire 64 bits par cycle depuis le cache L1, ce qui fait un seul accès au cache par cycle. La file de µops mémoire est appelée la ''Pre-Cache Queue''. Si au vu de son nom, vous avez deviné qu'il y avait une ''Post-Cache Queue''. Elle mémorise les lectures/écritures émises, mais qui ont levé un défaut de cache L1. Elle ne fait pas partie de la file de µops mémoire proprement dite. Les architectures K7, K8 et K10 ont des unités de calcul très similaires. Concrètement, il y a trois ALU entières, trois unités de calcul d'adresse, et une FPU. Le processeur incorpore, aussi un multiplieur entier, relié sur le port d'émission de la première ALU. La FPU regroupe un additionneur flottant, un multiplieur flottant, et une troisième unité LOAD/STORE pour les lectures/écritures pour les nombres flottants. L'architecture K8 ajoute une unité de manipulation de bit, la K10 un diviseur entier. [[File:Unité de calcul des processeurs AMD K7, K8 et K10.png|centre|vignette|upright=2|Unité de calcul des processeurs AMD K7, K8 et K10]] La manière d'alimenter les ALU en micro-opérations varie un petit peu entre les architectures K7, K8 et K10. Il y a cependant quelques constantes entre les trois. La première est qu'il y a une fenêtre d'instruction séparée pour les flottants, de 36 à 42 entrées, avec renommage de registre. La fenêtre d'instruction flottante a trois ports d'émission : un pour l'additionneur flottant, un autre pour le multiplieur, et un troisième pour la troisième unité flottante qui s'occupe du reste. La seconde est que chaque ALU entière est couplée avec une unité de calcul d'adresse. Par contre, la méthode de couplage varie d'un processeur à l'autre. : Les stations de réservation sont nommées des ''schedulers'' dans les schémas qui suivent. La microarchitecture K7 avait deux fenêtres d'instruction : une pour les opérations flottantes, une autre pour les instructions entières et les accès mémoire. La fenêtre d'instruction entière était reliée à 3 ALU entières et à 3 AGU. Elle pouvait émettre trois micro-opérations en même temps : trois micro-opérations entières, trois micro-opérations mémoire. Les AGU étaient reliées à la file de µops mémoire mentionnée plus haut, ce qui permet d'émettre trois µops mémoire par cycle. Par contre, la file de µops mémoire ne pouvait exécuter qu'une lecture de 64 bits ou une écriture de 64 bits. En clair, trois micro-opérations mémoire peuvent être émises par cycle, cela entraine trois calculs d'adresse simultanés, mais les trois lectures/écritures sont mises en attente dans la file de µops mémoire. Elles s'exécutent alors l'une après l'autre. La fenêtre d'instruction entière contenait 5 à 6 groupes de 3 macro-opérations. Vous noterez que j'ai parlé de macro-opérations et pas de micro-opérations, car les instructions ''load-up'' sont considérées comme une seule "micro-opération" dans la fenêtre d'instruction entière. Et cela se marie bien avec une fenêtre d'instruction unique partagée entre pipeline entier et pipeline mémoire. Une macro-opération était scindée en deux micro-opérations : une micro-opération mémoire et une micro-opération entière. Il est donc avantageux de regrouper unités mémoire et unités entières à la même fenêtre d'instruction pour ce faire. [[File:AMD K7.png|centre|vignette|upright=3|AMD K7]] Sur les architectures K8 et K10, la station de réservation unique de 15 micro-opérations est remplacée par trois stations de réservations, de 8 micro-opérations chacune pour le K8, de 10 pour le K10. Chaque station de réservation entière alimente une unité de calcul entière et une unité de calcul d'adresse. l'unité de calcul d'adresse est reliée à la file de µops mémoire, qui n’exécute toujours qu'un seul accès mémoire par cycle. Le multiplieur est relié à la première station de réservation, sur le même port d'émission que l'ALU. [[File:AMD Husky microarchitecture.png|centre|vignette|upright=3|AMD Husky microarchitecture]] La microarchitecture K10 a été déclinée en plusieurs versions, nommées Grayhound, Grayhound+ et Husky, Husky étant une architecture gravée en 32 nm dédiée aux processeurs A-3000. L'architecture Grayhound a plus de cache et un ROB plus grand, la Husky est quand à elle un peu plus différente. Elle n'a pas de cache L3, contrairement aux autres architectures K10, ce qui simplifie fortement son sous-système mémoire. Par contre, les fenêtres d'instructions/stations de réservation et le ROB sont plus grands, pareil pour les files dans l'unité mémoire. Une ALU pour les divisions entières a aussi été ajoutée. Pour résumer, les architectures K7, K8 et K10 séparent les pipelines entiers et flottants : trois pipelines entiers avec chacun son unité de calcul, et un pipeline flottant avec plusieurs unités de calcul. Les raisons à cela sont assez diverses. Disons que dupliquer des ALU entières simples prend peu de transistors, là où les gros circuits comme le multiplieur ou la FPU ne sont pas dupliqués. Et cela a un autre avantage : le renommage, ''dispatch'' et l'émission sont plus simples. Les pipelines entiers ont une exécution dans le désordre peu complexe, grâce au grand nombre d'unités de calcul, ce qui fait que le pipeline entier est de seulement 15 cycles au total (chargement et décodage inclus). A l'opposé, la FPU est alimentée par une exécution dans le désordre très complexe, avec banc de registre physique et beaucoup de ressources, mais au prix d'un pipeline flottant plus long de 3 cycles, soit 18 cycles au total. ===Les microarchitectures ZEN d'AMD=== Viennent ensuite les '''microarchitectures Bulldozer''', avec trois révisions ultérieures nommées Piledriver, Steamroller et Excavator. Mais du fait de l'utilisation de techniques de multithreading matériel que nous n'avons pas encore abordé, nous ne pouvons pas en parler ici. Les microarchitectures suivantes sont les '''architectures ZEN 1/2/3/4/5'''. Elles se ressemblent beaucoup, chacune accumulant les améliorations des précédentes. Mais le cœur de l'architecture reste plus ou moins le même. En passant à la suivante, le nombre de registre virtuel augmente, le ''branch target buffer'' augmente en taille, le ROB et les files d'attente grossissent, les caches de micro-opération aussi, les caches grossissent, etc. Une optimisation intéressante est l'ajout d'un cache de micro-opération, qui améliore grandement les performances du ''front-end'', notamment pour les boucles. La microarchitecture Zen 1 est illustrée ci-dessous. Comme on le voit, les registres flottants ont une unité de renommage séparée de celle pour les entiers, mais les deux utilisent du renommage à banc de registre physique. Il y a par contre une différence au niveau des fenêtres d'instruction, notées ''scheduler'' dans le schéma. Pour ce qui est des unités de calcul flottantes, il y a une fenêtre unifiée qui alimente quatre ALU, grâce à 4 ports d'émission. Mais pour les ALU entières, il y a une fenêtre d'instruction par ALU, avec un seul port d'émission connecté à une seule ALU. La raison de ce choix est que les opérations flottantes ont un nombre de cycle plus élevé, sans compter que les codes flottants mélangent bien additions et multiplication. Une fois décodées, les instructions sont placées dans une première file de micro-opérations om elles attendent, puis sont dispatchées soit dans le pipeline entier, soit dans le pipeline flottant. les micro-opérations entières sont insérées dans une fenêtre d'instruction directement, alors que les micro-opérations flottantes doivent patienter dans une seconde file de micro-opérations. La raison est que les micro-opérations flottantes ayant une grande latence, trop d'instructions flottantes consécutives pourraient bloquer le pipeline flottant, sa fenêtre d'instruction étant pleine. Le pipeline flottant étant bloqué, la première file de micro-opérations serait bloquée et on ne pourrait plus émettre de micro-opérations entières. Pour éviter cela, une solution serait d'agrandir la file de micro-opérations, mais cela la rendrait plus lente et se ferait donc au détriment de la fréquence d'horloge. Alors une solution a été d'ajouter une seconde file de micro-opérations, au lieu d'agrandir la première. [[File:Zen microarchitecture.svg|centre|vignette|upright=3|Microarchitecture Zen 1 d'AMD.]] Le passage à la microarchitecture n'a pas causé de grands changements. Le Zen 2 a ajouté une unité de calcul d'adresse, ce qui fait qu'on passe à 4 ALU, 3 AGU et 4 FPU. La fenêtre d'instruction flottante reste la même. Par contre, les fenêtres d'instruction entières changent un peu. Ou plutot devrais-je dire les fenêtres d'instruction mémoire. En effet, le Zen 2 fusionne les fenêtres d'instructions liées aux AGU en une seule fenêtre d'instruction deux fois plus grosse. Le Zen 5 a ajouté deux autres ALU entières et une unité de calcul d'adresse (6 ALU / 4 AGU) ==Les processeurs Atom d'Intel, de microarchitecture Bonnell== L'architecture de l'Atom première génération est assez simple. Son pipeline faisait 16 étages, ce qui est beaucoup. C'est un processeur 32 bits, ce qui aura son importance dans ce qui suit. Il était conçu pour être un processeur basse consommation, donc peu puissant. En conséquence, il n'a pas d'exécution dans le désordre, même s'il est superscalaire. C'était la norme à l'époque pour les processeurs basse consommation, que de faire sans exécution dans le désordre. De nos jours, les choses ont bien changée, même les processeurs basse consommation ont exécution dans le désordre, superscalarité et renommage de registres. ===Le ''front-end'' de l'Atom=== Le cache d'instruction permet de lire 8 octets par cycle, qui sont placés dans une file d'instruction, elle-même suivie par deux décodeurs. Le fait que les décodeurs lisent les instructions depuis une file d'instruction fait que les deux instructions décodées ne sont pas forcément consécutives en mémoire RAM. Par exemple, l'Atom peut décoder un branchement prédit comme pris, suivi par l'instruction de destination du branchement. Les deux instructions ont été chargées dans la file d'instruction et sont consécutifs dedans, alors qu'elles ne sont pas consécutives en mémoire RAM. Sur l'Atom, la majorité des instructions x86 sont décodées en une seule micro-opération, y compris les instructions ''load-up''. Le microcode n'est utilisé que pour une extrême minorité d'instructions et est à part des deux décodeurs précédents. L'avantage est que cela permet d'utiliser au mieux la file de micro-opération, qui est de petite taille. Mais surtout, cela permet de grandement réduire la consommation du processeur, au détriment de ses performances. Pour avoir un décodage rapide, malgré des instructions complexes, le processeur recourt à la technique du pré-décodage, qui prédécode les instructions lors de leur chargement dans le cache d'instruction. Le prédécodage lui-même prend deux cycles, là où une lecture dans le L1 d'instruction en prend 3. les défauts de cache d'instruction sont donc plus longs de deux cycles. Mais l'avantage du prédécodage est que la consommation d'énergie est diminuée. Prenez une instruction exécutée plusieurs fois, dans une boucle. Au lieu de décoder intégralement une instruction à chaque fois qu'on l'exécute, on la prédécode une fois, seul le reste du décodage est fait à chaque exécution. D'où un gain d'énergie assez intéressant. Les caches de micro-opération, qui sont capables d'exécuter une optimisation similaire, n'existaient pas encore à cette époque. ===Le chemin de données de l'Atom=== Les deux décodeurs alimentent une file de micro-opérations de petite taille : 32 µops maximum, 16 par ''thread'' si le ''multithreading'' matériel est activé. La file de micro-opérations a deux ports d'émission, ce qui permet d'émettre au maximum 2 µops par cycle. Les conditions pour cela sont cependant drastiques. Les deux instructions ne doivent pas avoir de dépendances de registres, à quelques exceptions près liées au registre d'état. Le multithreading matériel doit aussi être désactivé. Les deux instructions doivent aller chacun dans un port différent, et cela tient en compte du fait que les deux ports sont reliés à des unités de calcul fort différentes. Le tout est illustré ci-dessous. Les deux ports ont chacun une ALU simple dédiée, capable de faire des additions/soustractions, des opérations bit à bit et des copies entre registres. Mais ils ont aussi des opérations qui leur sont spécifiques. La séparation entre les deux pipelines est assez complexe. Il ne s'agit pas du cas simple avec un pipeline entier et un pipeline flottant séparés. En réalité, il y a deux pipelines, chacun capables de faire des opérations entières et flottantes, mais pas les mêmes opérations. Le premier port permet d’exécuter des opérations entières simples, une addition flottante, des comparaisons/branchements, ou une instruction de calcul d'adresse LEA. Le second port/pipeline est, quant à lui, conçu pour exécuter les instruction ''load-up'' nativement, en une seule micro-opération. Il contient toute la machinerie pour faire les accès mémoire, notamment des unités de calcul d'adresse et un cache L1 de données. A la suite du cache, se trouvent une ALU entière simple, un ''barrel shifter'', et un circuit multiplieur/diviseur. Le circuit multiplieur/diviseur est utilisé à la fois pour les opérations flottantes et entières. [[File:Intel Atom Microarchitecture.png|centre|vignette|upright=2.5|Intel Atom Microarchitecture]] Cette organisation difficile à comprendre est en réalité très efficace, très économe en circuit, tout en gardant une performance intéressante. Les instructions simples, ADD/SUB/bitwise sont supportées dans les deux pipelines. Il faut dire que ce sont des opérations courantes qu'il vaut mieux optimiser au mieux. Le processeur peut donc émettre deux opérations simples et fréquentes en même temps, ce qui augmente les performances. Les opérations plus complexes, à savoir les multiplications/divisions/décalages/rotations/manipulations de bit sont supportées dans un seul pipeline. La raison est qu'il est rare que de telles opérations soient consécutives, et qu'il n'est donc pas utile d'optimiser pour cette situation. Si les deux pipelines devaient supporter ces opérations, cela demanderait de dupliquer les circuits multiplieurs/diviseur, ce qui aurait un cout en circuit important pour un gain en performance assez faible. ===Le système d'exceptions flottantes de l'Atom=== Le processeur étant sans exécution dans le désordre, ses instructions doivent écrire dans les registres dans l'ordre du programme. En conséquence, certaines instructions doivent être retardées, leur émission doit attendre que les conditions soient adéquates. Et cela pose problème avec les opérations flottantes, vu qu'elles prennent pas mal de cycles pour s'exécuter. Imaginez qu'une instruction flottante de 10 cycles soit suivie par une instruction entière. En théorie, on doit retarder l'émission de l'instruction entière de 9 cycles pour éviter tout problèmes. Le cout en performance est donc assez important. En théorie, les instructions entières et flottantes écrivant dans des registres séparés, ce qui fait que l'on pourrait exécuter instructions entières et flottantes dans le désordre. Sauf pour les instructions de copie entre registres entier et flottants, mais laissons-les de côté. Le problème est qu'une instruction flottante peut parfois lever une exception, par exemple en cas de division par zéro, ou pour certains calculs précis. Si une exception est levée, alors l'instruction flottante est annulée, de même que toutes les instructions qui suivent, y compris les opérations entières. Ce n'est pas un problème si le processeur gère nativement les exceptions précises, par exemple avec un tampon de ré-ordonnancement. Mais l'Atom étant un processeur sans exécution dans le désordre, les instructions entières devraient être mises en attente tant qu'une instruction flottante est en cours d'exécution. Heureusement, l'Atom d'Intel a trouvé une parade. La technique, appelée ''Safe Instruction Recognition'' par Intel, est décrite dans le brevet US00525721.6A. L'idée est de tester les opérandes flottantes, pour détecter les combinaisons d'opérandes à problème, dont l'addition/multiplication peut lever une exception. Si des opérandes à problème sont détectées, on stoppe l'émission de nouvelles instructions en parallèle de l'instruction flottante et l'unité d'émission émet des bulles de pipeline tant que l'instruction flottante est en cours. Sinon, l'émission multiple fonctionne. La technique permet ainsi de ne pas écrire dans les registres entiers/flottants dans l'ordre du programme : une instruction entière peut être autorisée à s'exécuter même si elle écrit dans un registre entier avant qu'une instruction flottante délivre son résultat. ==La microarchitecture Netburst du Pentium 4== Dans cette section, nous allons voir l'architecture du processeur Pentium 4, qu'on a volontairement laissée de côté précédemment. Pourquoi un tel saut dans le temps ? Parce que le Pentium est complément à part des autres architectures Intel. Le Pentium 4 a représenté une rupture en termes de microarchitecture, qui a été un échec tellement retentissant que les processeurs suivants sont repartis sur la base du Pentium 3. Il introduisait de nombreuses nouveautés architecturales qui étaient très innovantes. Par exemple, il introduisait le renommage avec un banc de registre physique, qui a été utilisé sur tous les processeurs Intel suivants. Mais la plupart de ces innovations étaient en réalité de fausses bonnes idées, ou du moins des idées difficiles à exploiter. Par exemple, le système de pipeline à ''replay'' n'a été utilisé que sur le Pentium 4 et aucun autre processeur ne l'a implémenté. ===Un focus sur la fréquence d'horloge=== La microarchitecture du Pentium 4 a été déclinée en plusieurs versions, dont les finesses de gravure n'étaient pas les mêmes. La microarchitecture Netburst, utilisée sur le Pentium 4, utilisait un pipeline à 20 étage, augmenté à 32 sur une révision ultérieure. Il a existé quatre révisions de l'architecture : Willamette (180 nm), Northwood (130 nm), Prescott (90 nm) et Cedar Mill (65 nm). Un point important est que le Pentium 4 était prévu pour fonctionner à haute fréquence. Ses 1,5 GHz étaient impressionnants pour l'époque, les autres processeurs tournant à une fréquence proche du GigaHertzs. Pour cela, la solution retenue par Intel a été un pipeline très long, avec beaucoup d'étages. Le Pentium 4 a été décliné en plusieurs versions assez proches, chacune avec sa propre finesse de gravure qui n'ont pas toute le même pipeline. Les micro-architectures ''Willamette'' et ''Northwood'' avaient un pipeline de 20 étages, alors que les autres processeurs de l'époque avaient entre 10 et 15 étages maximum. Les micro-architectures ''Prescott'' et ''Cedar Mill'' étaient une refonte qui a fait grimper le nombre d'étages à 31 ! Du jamais vu, il s'agit d'un record pour un processeur commercial. Un pipeline aussi long permet d'exécuter beaucoup d’instructions en même temps, chacune dans un étage, mais aussi d'atteindre de hautes fréquences facilement. Le problème est qu'un pipeline avec autant d'étages a beaucoup de problèmes. Un point important est que la prédiction de branchement est cruciale. Pour rappel, la pénalité en cas de mauvaise prédiction dépend du nombre d'étages avant que le branchement soit résolu. Et les branchements sont résolus soit en fin de décodage, soit dans l'unité de calcul. C'est à dire au milieu du pipeline, soit en fin de pipeline. La pénalité en cas de mauvaise prédiction de branchement était énorme sur le Pentium 4, elle atteignait facilement 30 cycles Pour compenser, le Pentium 4 avait une prédiction de branchement très performante, pour l'époque. J'insiste sur le pour l'époque. Il utilisait un prédicteur qu'on a déjà abordé dans le chapitre sur la prédiction de branchement, précisément un prédicteur adaptatif à deux niveaux avec un historique global de 16 bits. Il avait aussi un ''Branch Target Buffer'' de 4096 entrées. Mais surtout, il intégrait une sorte de précurseur du cache de micro-opération, appelé le cache de traces, qui est détaillé dans la section suivante. ===Le cache de trace du Pentium 4=== Les décodeurs du Pentium 4 ne font pas décoder les instructions, ils mémorisent le résultat dans un cache de micro-opération un peu particulier, appelé le '''cache de trace'''. Une ligne de cache peut mémoriser 6 micro-opérations, ce qui peu sembler peu mais a été repris sur les micro-architectures suivantes. Mais le cache de trace a une grande différence avec un cache de micro-opération normal. Un cache de micro-opération normal mémorise une instruction par ligne de cache. Une instruction est décodée en plusieurs micro-instructions, qui sont enregistrées dans une ligne de cache. Si l'instruction n'utilise par les 6 micro-opérations disponibles, le reste de la ligne de cache n'est pas utilisé. Mais le Pentium 4 optimise le tout de manière ce à ce que ne soit pas le cas. Sur le Pentium 4, la contrainte du "une instruction par ligne de cache" est abandonnée. Une ligne de cache mémorise 6 micro-opérations consécutives, qui peuvent appartenir à plusieurs instructions. Par exemple, si le décodeur décode 4 instructions consécutives en 6 micro-opérations au total, alors le tout prendra une seule ligne de cache sur le Pentium 4. Et les 4 instructions consécutives n'ont même pas à être consécutives en mémoire : il peut y avoir des branchements pris entre ces instructions ! {|class="wikitable" |+ Cache de trace |- ! Ligne de cache | ADD || SUB || ADD || MOV || MUL || ''shift'' |- ! Ligne de cache | colspan="3" | ADD ''load-up'' || MUL || colspan="2" | Branch if Equal |- ! Ligne de cache | XOR || colspan="4" | POP || SUB |- ! ... | colspan="6" | ... |} Pour expliquer cela plus concrètement, nous allons devoir introduire les concepts de trace et de bloc de base. Un '''bloc de base''' (''basic block'') est une suite d'instructions sans branchement, qui est séparé par deux branchements. Le début d'un bloc de base est la destination d'un branchement, un bloc de base se termine avec un branchement. Une '''trace''' est formée en concaténant plusieurs blocs de base. Pour donner un exemple, regardez le code illustré ci-contre. Il est composé d'un bloc de base A, suivi par un bloc de base B, qui peut faire appel soit au bloc C, soit un bloc D. Un tel code peut donner deux traces : ABC ou ABD. La trace exécutée dépend du résultat du branchement qui choisit entre C et D. Le cache de trace mémorise des traces de 6 micro-opérations consécutives. Les traces sont formées en sortie des décodeurs d'instruction, par de subtiles opérations mélangeant mémorisation, décalage et concaténation. Les circuits qui construisent les traces ne sont pas connus, mais ils doivent certainement être très compliqués. toujours est-il qu'un cache de trace peut mémoriser des traces différentes, même si leur début est le même. Par exemple, prenons deux traces, composées des blocs de base A, B, C et D. La première trace est la trace ABC, la seconde est la trace ABD. Les deux traces auront chacune une ligne de cache dédiée. Une trace est réutilisable quand le premier bloc de base est identique et que les prédictions de branchement restent identiques. Pour vérifier cela, le tag du cache de traces contient l'adresse du premier bloc de base, la position des branchements dans la trace et le résultat des prédictions utilisées pour construire la trace. Le résultat des prédictions de branchement de la trace est stocké sous la forme d'une suite de bits : si la trace contient n branchements, le n-ième bit vaut 1 si ce branchement a été pris, et 0 sinon. Même chose pour la position des branchements dans la trace : le bit numéro n indique si la n-ième instruction de la trace est un branchement : si c'est le cas, il vaut 1, et 0 sinon. Si la trace est réutilisée par la suite, elle est lue depuis le cache de traces. Pour savoir si une trace est réutilisable, l'unité de chargement envoie le ''program counter'' au cache de traces, l'unité de prédiction de branchement fournit le reste des informations. Si on a un succès de cache de traces, et la trace est envoyée directement au décodeur. Sinon, la trace est chargée depuis le cache d'instructions et assemblée. Il faut signaler que le cache de trace avait sa propre unité de prédiction de branchement séparée de l'unité de prédiction de branchement normale. [[File:TraceCache.png|centre|vignette|upright=2|Cache de traces.]] Le cache de traces réduisait la longueur du pipeline en cas de succès de cache de trace. Quand les instructions étaient lues depuis le cache de trace, les étages avant le cache de trace ne sont pas utilisés, tout se passe comme s'ils étaient retirés du pipeline. C'est la même chose avec le cache de micro-opération des processeurs modernes, mais l'idée n'existait pas encore à l'époque. Le cache de trace mémorise des traces décodées, ce qui fait qu'un succès de cache de trace contournait non seulement le cache d'instruction, mais aussi les décodeurs. Le temps d'accès au cache de trace pouvait être assez élevé, même s'il était comparable au temps d'accès du cache d'instruction. Le cache de traces a depuis été remplacé par une alternative bien plus intéressante, le cache de micro-opérations, plus flexible et plus performant. Comparé à un cache de trace, la contrainte "une instruction par ligne de cache" simplifie grandement l'implémentation d'un cache de micro-opération. Ne parlons pas de la détection des succès de cache, qui demande d'utiliser les prédictions de branchement. Mais le vrai problème avec le cache de trace est tout autre. Il arrive souvent qu'une micro-opération soit présente dans plusieurs lignes de cache en raison du processus de construction des traces, chose impossible avec un cache de micro-opération. Et c'est un problème, qui réduit la capacité effective du cache de trace. Alors certes, une ligne de cache est plus remplie que sur un cache de micro-opération, on est certain que les 6 micro-opération par ligne de cache sont remplies. Mais la redondance réduit grandement cet avantage. ===L'exécution dans le désordre et le chemin de données du P4=== Le renommage de registres se fait avec un banc de registres physiques avec une table d'alias. Le Pentium 4 avait une exécution dans le désordre très limitée, basée sur la présence de deux files de micro-opération : une pour les accès mémoire, une autre pour les autres instructions. La seconde file regroupait opérations entières et flottantes, elles n'étaient pas séparées. Avec ces deux files, les instructions mémoire étaient exécutées dans l'ordre du programme, les instructions arithmétiques s'exécutaient aussi dans l'ordre du programme, mais une instruction arithmétique pouvait passer avant une instruction mémoire et inversement. L'avantage est que cela permettait de faire des lectures en avance, c'était une forme limitée de lecture non-bloquantes. Il s'agit bel et bien de deux files d'instructions, pas de fenêtres d'instruction ni de stations de réservation. Le Pentium 4 est le seul processeur commercial qui a utilisé des files de micro-opération séparées, tous les autres utilisent des fenêtres d'instruction : centralisées pour Intel, décentralisées pour AMD (en général). Le Pentium 2 et 3, bien qu'antérieurs, utilisait une station de réservation unique. Cela peut sembler être un retour en arrière, mais les files d'instructions sont bien plus larges : de 42 micro-opérations pour le Pentium 3, on passe à 120 micro-opérations pour le Pentium 4. Et vu la longueur du pipeline, qui fait qu'il y a plus d'instructions en vol, c'était une nécessité. Mais cela n'aurait pas été possible en utilisant des stations de réservation, pour des raisons de consommation électrique et/ou de budget en transistors, ce qui fait que passer à une file de micro-opération été la solution retenue. Pour les accès mémoire, le Pentium 4 utilisait donc une file de µops mémoire unique, couplée à une file d'écriture (non-représentée sur les schémas qui suivent). La file d'écriture du Pentium 4 était de 24 écritures maximum et gérait le ''Store-to-load forwarding''. Le processeur pouvait émettre une lecture et une écriture à chaque cycle. Il y avait un port d'émission pour les lectures et un autre pour les écritures, tous deux ayant chacun leur propre unité de calcul d'adresse. Le processeur contenait 3 ALU entières, 2 unités de calcul d'adresse et une FPU. La FPU était complétée par une unité pour faire des copies entre registres flottants, des opérations MOV. Pour les AGU, il y en avait une dédiée aux lectures, une autre pour les écritures. Le tout était relié aux ports d'émissions comme suit : [[File:Ports d'émission du Pentium 4.png|centre|vignette|upright=2.5|Ports d'émission du Pentium 4]] Le processeur utilisait deux réseaux de contournement séparés : un pour les opérations flottantes, un pour les opérations entières. Le réseau de contournement pour les opérations entières est aussi relié aux unités de calcul d'adresse. Jusque là, rien de surprenant, le chemin de données du processeur est assez classique. [[File:Architettura Pentium 4.png|centre|vignette|upright=3|Microarchitecture du Pentium 4.]] ===Les unités de calcul entières du Pentium 4=== Sur le Pentium 4, les ALU entières étaient cadencées à une fréquence double de celle du processeur. Les ALU entières pouvaient exécuter deux micro-opérations par cycle, ce qui fait que les ports d'émissions reliés aux ALU devaient eux aussi fonctionner à double fréquence. Pour faire la différence entre les deux fréquences, nous parlerons de fréquence/cycle processeur et de fréquence/cycle de l'ALU. Précisons que seules les ALU entières étaient à double fréquence, pas le multiplieur, pas le ''barrel shifter''. Pour simplifier, nous allons parler d'additionneur plutôt que de l'ALU entière, ce qui sera plus proche de la réalité. Et l'implémentation de l'additionneur du Pentium 4 était très innovante. L'additionneur pouvait exécuter deux additions par cycle, même si les deux additions ont une dépendance. Mais n'allez pas croire que l'implémentation était intuitive, avec un additionneur 32 bit basique très rapide. Non seulement l'additionneur fonctionnait à double fréquence, mais il était aussi pipeliné, avec un système de contournement interne ! Les additionneurs étaient pipelinées, d'une manière très simple. Une addition 32 bits était découpée en trois étapes : deux additions de 16 bits, une dernière étape pour mettre à jour le registre d'état. Pour cela, chaque additionneur était composé de deux additionneur 16 bits chacune, placées l'une après l'autre, avec un registre de pipeline entre les deux. L'additionneur prenait deux cycles d'horloge pour faire son travail : le premier cycle calculait les 16 bits de poids faible, le second calculait les 16 bits de poids fort lors du second cycle. Le tout est appelé '''addition étagée''' (''staggered add'') dans la documentation Intel. Une addition se fait donc en deux étapes, sauf que c'est compensé par le fait que l'additionneur fonctionnait à une fréquence double de celle du processeur ! Le résultat de ce fonctionnement franchement bizarre, est que les 16 bits de poids faible étaient calculés en une moitié de cycle processeur, alors que l'opération complète prenait un cycle. Deux additions consécutives s'exécutaient donc en 1 cycle et demi, alors qu'on aurait cru au premier abord que cela prendrait seulement un cycle. Si on fait les calculs, on s'apercoit que le rythme de croisière est cependant proche de 2 additions par cycle, bien qu'inférieur. 3 additions consécutives se font en deux cycles, 5 additions en 3 cycles, 7 en 4 cycles, etc. Et le Pentium 4 ajoutait un système de contournement interne à l'ALU. En clair, si une addition utilise le résultat de l'addition précédente, les deux peuvent s'exécuter en un cycle d'horloge et demi. Les 16 bits de poids faible de la première addition sont disponibles après un cycle ALU, ce qui permet de démarrer le calcul de la seconde addition au cycle suivant. ===Le ''replay pipeline''=== Le processeur est un processeur triple émission : il peut charger et décoder 3 µops par cycle, le ROB peut terminer 3 µops par cycle, etc. Pourtant, les ports d'émission peuvent émettre 6 instructions par cycle : 4 ports, dont deux à double fréquence. Une telle différence s'explique par l'usage d'un ''replay pipeline'', dont nous avons déjà parlé dans ce cours. Pour rappel, le Pentium 4 suppose que les lectures font tous un succès de cache L1. Si une opération arithmétique utilise la donnée lue comme opérande, le processeur l'émet immédiatement, l'opérande sera disponible une fois l'opération en entrée de l'ALU entière. Mais s'il s'est trompé, le processeur ré-exécute l'instruction après un temps d'attente de quelques cycles, pour se caler sur la latence du cache L2. Et si il y a un défaut de cache L2, l’instruction attend encore. Cela demande de ré-exécuter des instructions émises à tord, ce qui fait que le processeur doit avoir la capacité d'exécution pour. Ce pourquoi le processeur peut émettre 6 µops dans les unités de calcul : 3 µops normales et 3 µops ré-exécutées. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Les processeurs superscalaires | prevText=Les processeurs superscalaires | next=Les processeurs VLIW et EPIC | nextText=Les processeurs VLIW et EPIC }} </noinclude> pzrpz18n173nynmbh3cslowpj2lsqiz 772754 772753 2026-09-21T21:15:22Z Mewtow 31375 /* Les microarchitectures récentes d'Intel */ 772754 wikitext text/x-wiki Dans ce chapitre, nous allons étudier des exemples de processeurs x86, ceux présents dans nos PC. Nous n'allons pas voir les anciens processeurs comme le 286, le 386 ou le 486. Nous allons commencer avec le Pentium 1, et les processeurs commerciaux qui ont suivis. Tous les processeurs que nous allons voir dans ce chapitre sont des processeurs superscalaires. De fait, ce n'est pas pour rien si ce chapitre se situe après le chapitre sur les processeurs superscalaires. Par contre, nous allons voir que certains n'ont pas d'exécution dans le désordre. La raison est que l'exécution dans le désordre est arrivé après la superscalarité. ==Généralités sur les CPU x86 superscalaires== Avant de voir chaque processeur indépendamment des autres, nous allons devoir aborder quelques généralités. Nous allons d'abord voir que le jeu d'instruction x86 pose quelques problèmes pour la superscalarité. Et ces problèmes posent des contraintes assez fortes, avec lesquelles les concepteurs de processeurs dovient faire avec. Nous poursuivrons ensuite par un historique des processeurs Intel et AMD, histoire de donner un peu de contexte aux processeurs que nous allons étudier. ===Le jeu d'instruction x86 pose des problèmes pour la superscalarité=== Une difficulté de l'architecture x86 est qu'il s'agit d'une architecture CISC, avec tous les défauts que ça implique. Un jeu d'instruction CISC a en effet de nombreuses propriétés qui collent mal avec l'émission multiple, avec la '''superscalarité'''. Il y en a plusieurs, certaines impactent le chargement des instructions, d'autres leur décodage, d'autres l'exécution, etc. Premièrement, les instructions sont de longueur variable, entre 1 et 15 octets, ce qui complique leur chargement et leur décodage. En pratique, les processeurs chargent un bloc de 32 à 64 octets, et découpent celui-ci en plusieurs instructions. La conséquence est que l'usage d'instructions trop longues peut poser problème. Imaginez qu'un processeur charge un bloc de 16 octets et que celui-ci ne contienne qu'une seule instruction : on ne profite pas de la superscalarité. Deuxièmement, une partie des instructions est microcodée, faute de mieux. Et cela pose de sérieux challenges pour l'implémentation des décodeurs. Dupliquer le microcode demanderait trop de transistors, ce qui fait que ce n'est pas fait. À la place, il n'y a qu'un seul microcode, ce qui fait que l'on ne peut pas décoder plusieurs instructions microcodées en même temps. Il est cependant possible de profiter de la superscalarité, en décodant une instruction microcodée en parallèle d'autres instructions non-microcodées. Et heureusement, ce cas est de loin le plus fréquent, il est rare que plusieurs instructions microcodées se suivent. Troisièmement, la présence d'instructions ''load-up'', qui lisent un opérande en mémoire, peut poser problème, mais est aussi source d'optimisations assez intéressantes. En théorie, une instruction ''load-op'' est décodée en deux micro-opération : une pour lire d'opérande en RAM, l'autre pour faire l'opération arithmétique. Sauf que les processeurs x86 modernes optimisent la gestion des instructions ''load-up''. Par exemple, les premiers processeurs Atom géraient des micro-opérations de type ''load-up'', directement dans le chemin de données ! D'autres processeurs utilisent la technique de la '''micro-fusion''' pour retarder le décodage réel des instructions ''load-up'' assez loin dans le pipeline. Avec eux, une instruction ''load-op'' est décodée en une seule "macro-opération", qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. L'avantage est qu'une macro-opération ne prend qu'une seule entrée dans le tampon de ré-ordonnancement, la fenêtre d'instruction, la file de micro-opération, et les autres structures similaires. ===Un petit historique des processeurs x86 superscalaires=== Nous allons voir les processeurs Intel à part des processeurs AMD. La raison à cela est que les architectures Intel et AMD ont progressivement évolué, chacune se basant sur la précédente et l'améliorant. Il n'y a pas eu de cassure entre microarchitectures AMD, qui sont chacune la suite de la précédente. Il est donc préférable de voir les architectures AMD dans l'ordre chronologique. Par contre, Intel a eu une gigantesque cassure, avec le processeur Pentium 4. Son architecture se démarquait fortement du Pentium 3, mais elle n'a pas convaincu et a été abandonnée avec les processeurs suivants. Ce qui fait nous verrons l'architecture du Pentium 4 à part. Le Pentium 1 et 2 utilisaient la même architecture, qu'on détaillera dans ce qui suit. Les seules différences importantes étaient la fréquence, le cache, et quelques détails dans le genre. Le Pentium 3 était une nouvelle microarchitecture qui ajoutait l'exécution dans le désordre. Un an et demi plus tard, le Pentium 4 est sorti et a été un échec. Ses performances étaient peu convaincantes face au Pentium 3, et sa consommation énergétique était très importante. La conséquence est que le Pentium 4 et le Pentium 3 ont survécu pendant un long moment, beaucoup de monde préférait acheter un Pentium 3. Intel a alors amélioré les microarchitectures du Pentium 3 et du 4, indépendamment, pendant environ 7 ans. La microarchitecture du Pentium 3 a subit plusieurs micro-évolutions, chacune avec une finesse de gravure différente, afin de satisfaire les consomateurs. Les premeirs Pentium 3 avaient une finesse de gravure de 250 nm, elle a chuté à 65 sur les derniers modèles. L'architecture du Pentium 4 a fait la même chose, pour tenter de corriger ses problèmes de performance et de consommation d'énergie. Les premiers Pentium 4 avaient uen finesse de gravure de 180 nm, elle a elle aussi chutée à 65 sur les derniers modèles. Après l'échec du Pentium 4, les ingénieurs d'Intel ont repris l'architecture P6 et l'ont améliorée fortement, pour donner l'architecture Core. Les micro-processeurs suivants ont fait évoluer cette architecture progressivement, au point où elle ne ressemble plus à l'originale. L'architecture Core a laissé la place à l'architecture Nehalem, puis Sandy Bridge, puis Haswell, puis Skylake, puis Ice Lake, et Golden Cove. Il s'agit de la lignée principale, partant du Pentium 3 et continuant jusqu'à nos jours. Ces microarchitectures ont suivi un motif assez simple, appelé modèle '''tick-tock'''. Chaque microarchitecture était déclinée en deux versions, la seconde ayant une finesse de gravure réduite. En parallèle, Intel a travaillé sur des processeurs basse performance et basse consommation, avec une microarchitecture très différente. Les processeurs Atom de microarchitecture Bonnel, pour être ensuite remplacés par les microarchitectures Silvermont, puis Goldmont et Gracemont. Ces microarchitectures ont évolué en parallèle de la lignée principale, il s'agit d'une lignée secondaire. Le tout est résumé dans ce schéma ci-dessous. [[File:IntelProcessorRoadmap-4v.svg|centre|vignette|upright=2.5|Roadmap des processeurs Intel, qui servira de structure pour la suite du chapitre]] De nos jours, les processeurs Intel utilisent les deux types de micro-architectures en même temps. Les CPU Intel modernes disposent de deux types de coeurs : les coeurs P et le coeurs E. Leur nom signifie "Performance" et "Efficient", qui trahissent leur but. Les coeurs P utilisent la microarchitecture de la lignée principale, qui est conçue pour la performance. Les coeurs E, quant à eux, utilisent les micro-architectures basse consommation de la lignée secondaire. ==Les processeurs x86 d'Intel, la lignée principale== Pour commencer, nous allons voir les processeurs Intel. N'y voyez pas du favoritisme derrière ce choix, la justification est toute autre. Si je commence par Intel, c'est pour commencer avec les Pentium 1 et 2, qui sont plus simples que tous les autres. Il s'agit en effet de processeurs superscalaires, mais sans exécution dans le désordre. L’absence d'exécution dans le désordre les rend bien plus simples à étudier que les autres, ce qui en fait un bon point de départ pour ce chapitre. Le successeur du Pentium 2 a intégré l'exécution dans le désordre, ce qui fait que les Pentium 1 et 2 sont les seuls processeurs superscalaires ''in-order''. Pour la concurrence, AMD n'a pas produit de processeurs ''in-order'', tous les processeurs produits par AMD intègrent l'exécution dans le désordre. A la rigueur, il y a bien les processeurs Atom de la lignée secondaire d'Intel. Cependant, même s'ils sont bien des CPU ''in-order'', leur architecture est assez compliquée. En comparaison, le Pentium est une vieille architecture, qui se débrouillait avec peu de transistors et était donc bien plus simple que celle de l'Atom. ===Le Pentium 1/MMX et les pipelines U/V=== Le processeur Pentium d'Intel avait un pipeline de 5 étages : un étage de chargement/prédiction de branchement, deux étages de décodage, un étage d'exécution et un dernier étage pour l'écriture dans les registres. Le Pentium 1 était un processeur double émission, intégrant deux pipelines nommés U et V. Chose importante, les deux pipelines n'étaient pas identiques. Le pipeline U pouvait exécuter toutes les instructions, mais le pipeline V était beaucoup plus limité. Par exemple, seul le pipeline U peut faire des calculs flottants, le pipeline V ne fait que des calculs entiers et des branchements. Les deux pipelines disposaient d'une unité de calcul entière, identique dans les deux pipelines. Mais le pipeline U incorporait un circuit multiplieur/diviseur et d'un ''barrel shifter''. L'unité flottante était sur le port d'émission du pipeline U, idem pour l'unité de calcul vectoriel MMX sur le Pentium MMX. Les deux pipelines avaient chacun une unité de calcul d'adresse, mais ils n'étaient pas identiques : celle du pipeline V ne gérait que l’instruction LEA, celle du pipeline U gérait tous les calculs d'adresse. {|class="wikitable" |- ! Pipeline U ! Pipeline V |- | ALU entière | ALU entière |- | Multiplieur/diviseur | |- | ''Barrel Shifter'' | |- | AGU complexe | AGU simple (opération LEA) |- | FPU | |- | Unité SIMD | |} Les deux pipelines géraient les opérations bit à bit, les additions, les soustractions et les comparaisons. Les autres instructions ne sont exécutables que dans le pipeline U. Pour être plus précis, les deux pipelines supportaient les instructions suivantes, ce qui fait qu'on pouvait en faire deux en même temps : * Les instructions arithmétiques INC, DEC, ADD, SUB ; * l'instruction de comparaison CMP ; * les instructions bit à bit AND, OR, XOR ; * l'instruction de calcul d'adresse LEA ; * l'instruction MOV (dépend du mode d'adressage) ; * les instructions de gestion de la pile PUSH et POP (dépend du mode d'adressage) ; * l'instruction NOP, qui ne fait rien. Il faut noter qu'il y a cependant quelques restrictions, beaucoup de paires d'instructions sont interdites. La plupart interdisent au pipeline V de faire quoique ce soit quand une opération particulière est émise dans le pipeline U. Par exemple, si le pipeline U exécute une multiplication ou une division, le processeur ne peut pas exécuter une opération dans le pipeline V. Et c'est pareil avec les branchements : si un branchement est émis dans le pipeline U, l'instruction suivant le branchement n'est pas émise dans le pipeline V, pour éliminer les dépendances de contrôle. De même, si le pipeline U exécute une opération flottante, le pipeline V ne pourra rien exécuter. La seule exception est l'instruction FCXH, qui échange deux registres flottants. [[File:Intel Pentium arch.svg|centre|vignette|upright=2.5|Microarchitecture de l'Intel Pentium MMX. On voit que certaines unités de calcul sont dupliquées.]] Un choix assez intéressant a été fait pour le cache de données. Nous avions vu dans le chapitre sur les CPU superscalaires que la superscalarité a un impact sur l'unité mémoire. Il y a alors deux implémentations. La première ne fait rien, l'unité mémoire ne change pas, et le processeur ne peut pas faire deux accès mémoire simultanés. La seconde duplique l'unité mémoire et les ports de lecture/écriture du cache, ce qui autorise des accès mémoire simultanés. Le Pentium 1 utilise une solution intermédiaire. Les ingénieurs d'Intel étaient partis à la base sur un cache totalement double port, pour obtenir des performances maximales. Mais diverses simulations et observations les ont fait changer d'avis. Les simulations ont montré qu'il est "rare" que les deux pipelines aient besoin de lire/écrire dans le cache en même temps. Et ils ont optimisé le cache de donnée pour en tenir compte. Le cache de données est partiellement multiport : simple port sur certains aspects, double port sur d'autres. Le cache est un cache splité, à savoir que les données et les ''tags'' sont séparés dans des mémoires séparées. La mémoire pour les ''tags'' est multiport, ce qui permet d'interroger les ''tags'' du cache deux fois par cycle. Un port est relié au pipeline u, un autre au pipeline V. Mais pour les données, le cache n'a qu'un seul port pour lire/écrire des données. Impossible donc de lire deux données en même temps, pour alimenter les deux pipelines. Il utilise cependant 8 banques permet d'accélérer des accès mémoire proches dans le temps, mais dans des cycles d'horloge différents. Entre les deux mémoires, il y a un circuit qui détecte les conflits, à savoir les situations où les deux pipelines accèdent en même temps au cache. S'ils veulent lire/écrire une donnée dans le même cycle, ce qui est impossible avec un seul port, le pipeline U a la priorité et le pipeline V attend le cycle suivant. Le circuit détecte aussi les conflits de banque, à savoir quand un pipeline accède à une banque en cours d'accès par l'autre pipeline (on rappelle qu'un accès au cache prend plusieurs cycles). Le circuit détecte aussi certaines dépendances mémoires, à savoir des accès consécutifs à la même adresse. : La TLB du processeur est aussi totalement double port. ===La microarchitecture P6 du Pentium 2/3=== Le Pentium 3 utilisait la '''microarchitecture P6''', qui a été dérivée dans de nombreuses variantes, dont les finesses de gravure n'étaient pas les mêmes. Il introduit une exécution dans le désordre simple, avec une fenêtre d'instruction centralisée, avec renommage dans le désordre dans le ROB (tampon de ré-ordonnancement), commandé par une table d'alias. C'était un processeur triple émission, soit une instruction de plus que la double émission du Pentium 1. Le pipeline passe de 5 étage sur le Pentium à 14 - 12 étages, dont le détail est le suivant : * Prédiction de branchement, deux cycles ; * Chargement des instructions, trois cycles ; * Décodage de l'instruction, deux cycles ; * Renommage de registre, un cycle ; * Copie des opérandes dans le tampon de ré-ordonnancement (lié au renommage de registre dans le ROB) ; * Dispath dans ou depuis la station de réservation. * Exécution de l'instruction ; * Écriture du résultat dans le ROB ; * Écriture dans le banc de registre physique. Les instructions sont chargées par blocs de 16 octets, avec un système de fusion de blocs pour gérer les instructions à cheval sur deux blocs. Lors d'un branchement, deux blocs doivent être chargés si l'instruction de destination n'est pas alignée sur 16 octets et cela cause un délai de un cycle d'horloge. Le décodage des instructions x86 était géré par plusieurs décodeurs. Il y avait trois décodeurs : deux décodeurs simples, et un décodeur complexe. Les décodeurs simples décodaient les instructions les plus fréquentes, mais aussi les plus simples, qui étaient décodées en une seule micro-opération. Les instructions CISC complexes étaient gérées uniquement par le décodeur complexe, basé sur un microcode, qui pouvait fournir jusqu'à 4 micro-opérations par cycle. Le tout est résumé avec la règle 4-1-1. La toute première instruction chargée depuis la file d'instruction va dans le premier décodeur simple. Si jamais le décodeur ne peut pas décoder l'instruction, l'instruction est redirigée dans un autre décodeur, avec un délai d'un cycle d'horloge. Les stations de réservations étaient regroupées dans une structure centralisée, en sortie de l'unité de renommage. Elles avaient 5 ports d'émission, qui étaient sous-utilisés en pratique. Niveau ALU, on trouve deux ALUs entières, une flottante, une unité pour les instructions SSE et autres, et trois unités pour les accès mémoire (regroupées en une seule unité dans le schéma ci-dessous). Les unités mémoire regroupent une unité de calcul d'adresse pour les lectures, une autre pour les écritures, et une unité pour la gestion des données à écrire. Les unités de calcul d'adresse sont des additionneurs à 4 opérandes, complétement différents des ALU entières. Les ALU entières sont deux unités asymétriques : une ALU simple, et une ALU complexe incorporant un multiplieur. Les deux peuvent exécuter des opérations d'addition, soustraction, comparaison, etc. [[File:P6 func diag.png|centre|vignette|upright=2|P6 func diag]] Les premiers Pentium 3 n'avaient pas de cache L2 dans le processeur, celui-ci était sur la carte mère. Mais il a été intégré dans le processeur sur la seconde version du Pentium 3, la version Coppermine. Le Pentium 3 a servi de base aux microarchitectures d'Intel qui ont suivi. Les changements à chaque nouvelle génération sont assez mineurs : la prédiction de branchement est améliorée, la taille des stations de réservation et du ROB augmente, idem avec les autres structures liées à l'exécution dans le désordre. Les processeurs Intel ont conservé une fenêtre d'instruction centralisée, alors qu'AMD utilise une autre méthode, comme nous allons le voir dans ce qui suit. Les seuls changements notables sont est le passage à un renommage dans le ROB à un renommage à banc de registre physique, ainsi que l'introduction du cache de micro-opération. Et ce sont des modifications qu'AMD a aussi faites, celle-ci étant clairement une bonne idée pour toutes les micro-architectures avec un budget en transistor suffisant. Il est intéressant de garder cela en tête, car une bonne partie des améliorations de chaque micro-architecture proviendra de là. ===La microarchitecture Core=== La '''microarchitecture Core''' fait suite au Pentium 4, mais reprend en fait beaucoup d’éléments du Pentium 2 et 3. Elle utilise la station de réservation unique avec renommage dans le ROB, provenant du Pentium 2/3. Elle supporte aussi les optimisations des opérations ''load-up'', avec notamment un support des macro-opérations mentionnées plus haut. Les améliorations sont assez diverses, mais aussi assez mineures. * Le processeur incorpore un cache L2, en plus des caches L1 déjà présents auparavant. * La prédiction de branchement a été améliorée avec notamment l'ajout d'une ''Fetch Input Queue''. * L'architecture Core passe à la quadruple émission, soit une instruction de plus que sur le Pentium 2 et 3. Pour cela, un quatrième décodeur est ajouté, il s'agit d'un décodeur simple qui ne fournit qu'une seule micro-opération en sortie. * Un ''stack engine'' et un ''Loop Stream Detector'' ont été ajoutés, ainsi que le support de la macro-fusion qui fusionne une instruction de test et le branchement qui suit en une seule micro-opération. * Les techniques de désambiguïsation mémoire sont implémentées sur cette micro-architecture. Il y a quelques modifications au niveau de l'unité de chargement. La file d'instruction a toujours ce système de fusion de blocs, sauf que les branchements ne causent plus de délai d'un cycle lors du chargement. La file d'instruction est suivie par un circuit de prédécodage qui détermine la taille des instructions et leurs frontières, avant de mémoriser le tout dans une file de 40 instructions. La station de réservation dispose de 6 ports d'émission, mais on devrait plutôt dire 5. Sur les 5, il y en a un pour les lectures, un pour les écritures. Les deux sont reliées à une ''Load/Store Queue'', appelée ''Memory Ordering Buffer''. Elle est elle-même reliée au cache de données par deux ports : un port de lecture et un port d'écriture. Les trois ports d'émission restants sont connectés aux unités de calcul. Les trois ports récents sont chacun reliés à une ALU entière. L'additionneur flottant est connecté au port 1, alors que le multiplieur/diviseur flottante est connecté au port 0. Le fait de mettre les deux sur des ports différents permet d'émettre une addition et une multiplication flottant simultanément. Le multiplieur entier est relié au second port d'émission, celui sur lequel se trouve l'additionneur flottant. Le résultat que le processeur peut émettre un mix d'opérations flottantes et entière assez varié. [[Image:Intel Core2 arch.svg|centre|vignette|upright=2|Intel Core microarchitecture]] ===Les microarchitectures Sandy Bridge and Ivy Bridge=== Les micro-architectures suivant la micro-architecture Core ont introduit quelques grandes modifications : le passage à un renommage à banc de registre physique, l'ajout d'un cache de micro-opérations (et d'un ''Loop Stream Detector''). L'ajout du cache de micro-opérations est un gros changement, particulièrement avec le jeu d’instruction x86. Le décodage des instructions est lent, couteux en énergie. Mais avec l'introduction du cache de micro-opération, la majorité des micro-opérations est non pas décodée, mais lue depuis le cache de micro-opérations. Les décodeurs décodent les instructions pas encore exécutées, mais les exécutions suivantes sont lues depuis le cache de micro-opérations. Et vu la grande présence de boucles, le cache de micro-opérations est l'alimentation principale du pipeline. Les décodeurs servent surtout à alimenter le cache de micro-opérations, parfois décoder quelques instructions isolées exécutées de-dehors de boucles, pas plus. Concrètement, ils servent pour 10 à 20% des micro-opérations exécutées. Intel a d'ailleurs reflété ce fait dans sa terminologie. Intel distingue deux voies de chargement : le ''legacy pipeline'' et le cache de micro-opérations. L'unité de chargement et les décodeurs sont regroupés dans la voie du ''legacy pipeline''. Le cache de micro-opérations est complété avec un ''Loop Stream Detector'', placé après le cache en question. Les décodeurs et le cache de micro-opérations alimentent une file de micro-opérations, située juste avant l'étage de renommage de registres. La file de micro-opérations sert en quelque sorte de tampon entre l'étage de "décodage" et celui de renommage. Le ''Loop Stream Detector'' utilise cette file de micro-opérations comme d'un cache lorsqu'une boucle est détectée. Les micro-opérations de la boucle sont lue depuis la file de micro-opérations, pour être envoyée au renommeur de registres. L'avantage est que le cache de micro-opérations et/ou les décodeurs sont mis en pause et clock-gatés lorsqu'une boucle s'exécute, ce qui réduit la consommation du processeur. Le ''Loop Stream Detector'' et le cache de micro-opération ont globalement le même effet : désactiver tout ce qui est avant, le ''Loop Stream Detector'' appliquant cette méthode au cache de micro-opération lui-même.. Voyons maintenant quelles sont les micro-architectures qui implémentent ces optimisations. Les microarchitectures '''Sandy Bridge''' and '''Ivy Bridge''' sont similaires à l'architecture Core, si ce n'est pour le passage à un renommage à banc de registre physique, et l'ajout d'un cache de micro-opérations. Le nombre de ports d'émission passe à 7, avec 4 pour les instructions arithmétiques (flottantes comme entière), 2 pour les lectures, et un pour les écritures (en fait deux, avec un pour le calcul d'adresse, l'autre pour la donnée à écrire). Pour le reste, rien ne change si ce n'est la prédiction de branchement Les architectures '''Haswell''' et '''Broadwell''' ont ajouté quelques unités de calcul, élargit la sortie du cache de micro-opérations. Un port d'émission pour opération entières a été ajouté, de même qu'un port pour les accès mémoire. Le processeur passe donc à 8 ports d'émission, ce qui permet d'émettre jusqu'à 8 micro-opérations, à condition que le cache de micro-opération suive. Pour le reste, le processeur est similaire aux architectures précédentes, si ce n'est que certaines structures grossissent. L'architecture '''Skylake''' réorganise les unités de calcul et les ports d'émission pour gagner en efficacité. Pour le reste, les améliorations sont mineures. A la rigueur, l'unité de renommage de registre ajoute des optimisations comme l'élimination des MOV, les idiomes liés aux opérations avec zéro, etc. ===Les microarchitectures récentes d'Intel=== Les architectures '''Ice Lake''' et '''Tiger Lake''' passent de quadruple émission à la pentuple émission. Par contre, le processeur utilise toujours 4 décodeurs. Mais les micro-opérations étant émises depuis le cache de micro-opérations, ce n'est pas un problème pour la pentuple émission. Le processeur peut parfaitement émettre 5 micro-opérations en même temps, si elles sont lues depuis le cache de micro-opérations. Là encore, on voit à quel point le cache de micro-opération découple ce qu'il y avant de ce qu'il y a après. La microarchitecture '''Golden Cove''' altère les décodeurs et l'unité de chargement. Sur toutes les générations précédentes, on reste sur une unité de chargement qui charge 16 octets à la fois et il y a toujours 4 décodeurs identiques aux générations précédentes. Golden Cove passe à 6 décodeurs simples, et double la taille du chargement qui passe à 32 octets. Une telle stagnation sur les unités de chargement et de décodage s'explique encore une fois par la présence du cache de micro-opération fait que ce n'est pas trop un problème. Tout ce qui précède le cache de micro-opérations n'a pas de raison d'évoluer, car ce cache est très puissant. Niveau unités de calcul, le CPU a pas moins de 5 ALU entières, deux ''barrel shifters'', un multiplieur et deux unités de branchements. Le tout est répartit sur 5 ports d'émission. Pour les unités mémoire, il y a trois unités LOAD pour les lectures et deux unités STORE pour les écritures. Il n'y a plus d'unité flottante proprement dite, mais une unité SIMD qui est capable de faire plusieurs calculs flottants, qu'on ne détaillera pas ici, car nous n'avons pas encore vu les techniques de SIMD. Notons qu'il y a trois fenêtres d'instruction séparées pour : les instructions entières/flottantes, les lectures, les écritures. Le processeur peut décoder 6 instructions par cycle, et en émettre 6 à destination des fenêtres d'instructions. Les fenêtres d'instructions peuvent émettre 5 instructions entières, trois lectures et deux écritures en même temps. Le ROB accepte de ''commit'' 8 µops par cycle. [[File:Golden Cove.png|centre|vignette|upright=3|Golden Cove]] ==Un étude des microarchitectures superscalaires x86 d'AMD== Les architectures Intel ont évolué progressivement, sans grandes cassure. Il y a une continuité presque initerrompue entre l'architecture du Pentium 2 et les architectures modernes. Intel a fait des améliorations mineures à chaque nouvelle micro-architecture, si on omet le passage à un renommage à banc de registre physique et l'ajout du cache de micro-opération. A l'opposé, les architectures AMD ont eu de nombreuses cassures dans la continuité où AMD a revu sa copie de fond en comble. Étudier ces architectures demande de voir trois choses séparément : le ''front-end'' qui regroupe l'unité de chargement et les décodeurs, le ''back-end'' qui gère l'exécution dans le désordre et les unités de calcul, et le sous-système mémoire avec les caches et la ''Load Store Queue''. Leur étude sera plus ou moins séparée dans ce qui suit, pour chaque classe d'architecture. ===La première génération de CPU AMD : les architectures K5, K6, K7, K8 et K10=== La première génération de processeurs AMD est celle des architectures K5, K6, K7, K8 et K10. Il n'y a pas de K9, qui a été abandonné en cours de développement. Les processeurs K5 et K6 portent ce nom au niveau commercial. Par contre, les processeurs d'architecture K7 sont aussi connus sous le nom d''''AMD Athlon''', les AMD K8 sont connus sous le nom d''''AMD Athlon 64''', et les architecture K10 sont appelées les '''AMD Phenom'''. Comme le nom l'indique, l'architecture K8 a introduit le 64 bits chez les processeurs AMD. Elles ont une architecture assez similaire pour ce qui est du chargement et des caches. Toutes disposent d'au minimum un cache L1 d'instruction et d'un cache L1 de données. Le K5 n'avait que ces caches, mais un cache L2 a été ajouté avec le K7, puis un L3 avec le K10. L'AMD K5 avait une TLB unique, mais les processeurs suivants avaient une TLB pour le L1 d'instruction et une autre pour le L1 de données. Idem pour le cache L2, avec deux TLB : une pour les données, une pour les instructions. Les caches L1/L2 sont de type exclusifs, à savoir que les données dans le L1 ne sont pas recopiées dans le L2. Le cache L2 est précisément un cache de victime, qui mémorise les données/instructions, évincées des caches L1 lors du remplacement des lignes de cache. L'introduction du cache L2 a entrainé l'ajout de deux TLB de second niveau : une L2 TLB pour les données et une autre pour les instructions. Les architectures K8 et K10 ont ajouté un cache L3, avec un accès indirect à travers l'interface avec le bus. : L'AMD K7 originel, aussi appelée Athlon classique, n'avait pas de cache L2, mais celui-ci était placé sur la carte mère et fonctionnait à une fréquence moitié moindre de celle du CPU. L'Athlon Thunderbird, puis l'Athlon XP, ont intégré le cache L2 dans le processeur. {|class="wikitable" |- ! Architecture AMD ! colspan="5" | Caches |- | rowspan="2" | K5 | L1 instruction || L1 données || colspan="3" | |- | colspan="2" | TLB unique || colspan="3" | |- | colspan="4" | |- | rowspan="2" | K6 | L1 instruction || L1 données || colspan="3" | L2 unifié |- | TLB L1 instruction || TLB L1 données || colspan="3" | |- | colspan="6" | |- | rowspan="2" | K7, K8 | L1 instruction || L1 données || colspan="2" | L2 unifié || |- | TLB L1 instruction || TLB L1 données || TLB L2 instruction || TLB L2 données || |- | colspan="6" | |- | rowspan="2" | K10 | L1 instruction || L1 données || colspan="2" | L2 unifié || L3 |- | TLB L1 instruction || TLB L1 données || TLB L2 instruction || TLB L2 données || |} Fait important, les architectures K5 à K10 utilisent la technique du '''prédécodage''', où les instructions sont partiellement décodées avant d'entrer dans le cache d'instruction. Le prédécodage facilite grandement le travail des décodeurs d'instruction proprement dit. Par contre, le prédécodage prend de la place dans le cache L1 d'instruction, une partie de sa capacité est utilisé pour mémoriser les informations prédécodées. C'est donc un compromis entre taille du cache et taille/rapidité des décodeurs d'instruction. Sur les architectures K5 et K6, le prédécodage précise, pour chaque octet, si c'est le début ou la fin d'une instruction, si c'est un octet d'opcode, en combien de micro-opérations sera décodée l'instruction, etc. A partir de l'AMD K7, le prédécodage reconnait les branchements inconditionnels. Lorsqu'un branchement inconditionnel est pré-décodé, le pré-décodage tient compte du branchement et continue le pré-décodage des instructions à partir de la destination du branchement. Le système de prédécodage est abandonnée à partir de l'architecture Bulldozer, qui suit l'architecture K10. La prédiction de branchement de ces CPU tire partie de ce système de pré-décodage, à savoir que les prédictions de branchement sont partiellement mémorisées dans les lignes de cache du L1 d'instruction. Par exemple, l'AMD K5 se passe de ''Branch Target Buffer'' grâce à cela. Si une ligne de cache contient un branchement, elle mémorise l'adresse de destination de ce branchement, en plus des bits de pré-décodage. Si il y a plusieurs branchements dans une ligne de cache, c'est l'adresse de destination du premier branchement pris dans cette ligne de cache qui est mémoirsée. Un défaut de cette approche est que si le branchement n'est pas dans le L1 d'instruction, aucune prédiction de branchement ne peut être faite et le préchargement ne peut pas fonctionner. C'est une limitation que n'ont pas les BTB découplées du cache L1 : elles peuvent prédire un branchement qui a été évincé dans le L2 ou le L3, tant que l'entrée associée est dans le BTB. Les prédictions peuvent même servir à précharger les instructions utiles. [[File:Comparaison du chargement de l'AMD K5 et K6.png|centre|vignette|upright=2|Comparaison du chargement de l'AMD K5 et K6]] Au niveau du décodage, on trouve de nombreuses différences entre les premières architectures AMD. L'AMD K5 contient 4 décodeurs hybrides, afin de décoder 4 instructions par cycles. Le K5 a quatre décodeurs simples couplés à 4 décodeurs complexes avec chacun un accès au micro-code. Une instruction peut donc passer par a donc deux voies de décodage : un décodage rapide et simple pour les instructions simples, un décodage lent et passant par le microcode pour les instructions complexes. Pour décoder 4 instructions, les deux voies sont dupliquées en 4 exemplaires, ce qui a un cout en circuits non-négligeable. L'AMD K6 utilise moins de décodeurs et ne peut que décoder deux instructions à la fois maximum. Par contre, il fournit en sortie 4 micro-opérations. Il intègre pour cela deux décodeurs simples, un décodeur complexe et un décodeur micro-codé. Un décodeur simple transforme une instruction simple en une ou deux micro-opérations. Il est possible d'utiliser les deux décodeurs simples en même temps, afin de fournir 4 micro-opérations en sortie du décodeur. Les deux autres décodent une instruction complexe en 1 à 4 micro-opérations. Si jamais la ou les deux instructions sont décodées en 1, 2 ou 3 micro-opérations, les micro-opérations manquantes pour atteindre 4 sont remplies par des NOPs. Pour le K7 et au-delà, le processeur dispose de décodeurs séparées pour les instructions micro-codées de celles qui ne le sont pas. Le processeur peut décoder jusqu’à 3 instructions par cycle. Le décodage d'une instruction microcodée ne peut pas se faire en parallèle du décodage non-microcodé. C'est soit le décodeur microcodé qui est utilisé, soit les décodeurs câblés, pas les deux en même temps. Le décodage d'une instruction prend 4 cycles. Les instructions non-microcodées sont décodées en une seule micro-opération, à un détail près : le CPU optimise la prise en charge des instructions ''load-up''. La différence entre le K6 et le K7 s'explique par des optimisations des instructions ''load-up''. Sur le K6, les instructions ''load-up'' sont décodées en deux micro-opération : la lecture en RAM, l'opération proprement dite. Mais sur le K7, une instruction ''load-up'' est décodée en une seule micro-opération. En conséquence, les décodeurs simples sont fortement simplifiés et le décodeur complexe disparait au profit d'un microcode unique. [[File:Décodage sur le K5 et le K5.png|centre|vignette|upright=3|Décodage sur le K5 et le K5]] ====Les microarchitectures K5 et K6 d'AMD==== Les deux premières architectures étaient les architectures K5 et K6, l'architecture K6 ayant été déclinée en quatre versions, nommées K6-1, K6-2, et K-3, avec une version K6-3 bis. Elles sont regroupées ensemble car elles ont beaucoup de points communs. Par exemple, tout ce qui a trait au chargement et au cache était similaire, de même que les unités de calcul. Les deux architectures avaient n'avaient pas de cache L2 et devaient se contenter d'un cache L1 d'instruction et d'un cache L1 de données. L'AMD K5 incorpore une TLB unique, alors que le K6 utilise des TLB séparées pour le cache d'instruction et le cache de données. Une différence entre l'architecture K5 et K6 est que la première utilise des caches normaux, alors que la seconde utilise des ''sector caches''. Les deux architectures disposaient des unités de calcul suivantes : deux ALU entières, une FPU, deux unités LOAD/STORE pour les accès mémoire, une unité de branchement et une ou plusieurs unités SIMD. Une organisation classique, donc. Pour les unités entières, il y avait deux ALU simples, un ''barrel shifter'' et un diviseur. Il n'y a pas d'erreur, le processeur incorpore un circuit diviseur, mais pas de circuit multiplieur. La raison est que la multiplication est réalisée par la FPU ! En effet, le multiplieur flottant de la FPU intègre un multiplieur entier pour multiplier les mantisses, qui est utilisé pour les multiplications entières. La même technique a été utilisée sur l'Atom, comme vu plus haut. Le tout était alimenté par deux ports d'émission, appelés ports X et Y. Sur l'architecture K5, le ''barrel shifter'' et le diviseur sont des ports différents. {|class="wikitable" |+ AMD K5 |- ! Port X ! Port Y |- | ALU simple | ALU simple |- | ''Barrel Shifter'' | Diviseur |} Sur l'architecture K6, le ''barrel shifter'' et le diviseur sont sur le même port. {|class="wikitable" |+ AMD K6 |- ! Port X ! Port Y |- | ALU simple | ALU simple |- | | ''Barrel Shifter'' |- | | Diviseur |} Niveau unités mémoire, le K5 avait deux unités LOAD/STORE, chacune capable de faire lecture et écriture. Par contre, la ''store queue'' n'a qu'un seul port d'entrée, ce qui fait que le processeur peut seulement accepter une écriture par cycle. Le processeur peut donc émettre soit deux lectures simultanées, soit une lecture accompagnée d'une écriture. Impossible d'émettre deux écritures simultanées, ce qui est de toute façon très rare. L'architecture K6 utilise quant à elle une unité LOAD pour les lectures et une unité STORE pour les écritures. Ce qui permet de faire une lecture et une écriture par cycle, pas autre chose. Niveau unités SIMD, l'architecture K7 n'avait qu'une seule unité SIMD, placée sur le port d'émission X. L'architecture K8 ajouta une seconde unité SIMD, sur l'autre port d'émission entier. De plus, trois ALU SIMD ont été ajoutées : un décaleur MMX, une unité 3DNow!, une unité mixte MMX/3DNow. Elles sont reliées aux deux ports d'émission entier X et Y ! Elles ne sont pas représentées ci-dessous, par souci de simplicité. [[File:Unité de calcul des processeurs AMD K5 et K6.png|centre|vignette|upright=2|Unité de calcul des processeurs AMD K5 et K6. les unités sur la même colonnes sont reliées au même port d'émission.]] Si les unités de calcul et le chargement sont globalement les mêmes, les deux architectures se différencient sur l'exécution dans le désordre. L'AMD K5 utilise du renommage de registre dans le ROB avec des stations de réservation. Par contre, l'AMD K6 utilise une fenêtre d'instruction centralisée. De plus, son renommage de registre se fait avec un banc de registre physique. L'architecture AMD K5 utilisait de deux stations de réservation par unité de calcul, sauf pour les deux unités mémoire partageaient une station de réservation unique (deux fois plus grande). Les stations de réservation sont cependant mal nommées, vu que ce sont en réalité des mémoire FIFO. Une micro-opération n'est émise que si elle est la plus ancienne dans la FIFO/station de réservation. Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique. Le tampon de ré-ordonnancement faisait seulement 16 instructions. [[File:AMD K5.jpg|centre|vignette|upright=3|AMDK5 Diagramme.]] L'architecture K6 remplace les stations de réservations par une fenêtre d'instruction centralisée. Les 4 micro-opérations renommées sont écrites dans la fenêtre d'instruction par groupe de 4, NOP de ''padding'' inclus. La fenêtre d'instruction centralisé contient 24 micro-opérations, groupées en 6 groupes de 4 micro-opérations, avec potentiellement des NOP dedans suivant le résultat du décodage. L'avantage est que l'implémentation de la fenêtre d'instruction est simple. La fenêtre d'instruction centralisée permettait d'émettre 6 micro-opérations en même temps (une par unité de calcul/mémoire). Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique. Le processeur utilisait un renommage avec un banc de registre physique. Le banc de registre physique pour les entiers contenait 48 registres, dont 24 étaient des registres architecturaux et 24 étaient des registres renommés. Sur les 24 registres architecturaux, 16 avaient une fonction de ''scratchpad'' que les ''datasheets'' d'AMD ne détaillent pas, les 8 restants étaient les registres généraux EAX, EBX, etc. [[File:AMD K6 Little foot & Modl 6.png|centre|vignette|upright=3|AMD K6 original.]] ====Les microarchitectures K7, K8 et K10 d'AMD==== Les microarchitectures suivantes sont les architectures K7, K8 et K10. Les architectures K7, K8 et K10 sont assez similaires. La différence principale entre le K7 et le K8 est le support du 64 bits. Les apports du K10 sont la présence d'un cache L3, d'une unité de calcul supplémentaire et d'améliorations de la prédiction de branchement. La taille de certains caches a été augmentée, de même que la largeur de certaines interconnexions/bus. L'architecture K7 des processeurs Athlon utilisait le renommage de registre, mais seulement pour les registres flottants, pas pour les registres entiers. Le ranommeg des registres flottants étaient réalisé via un banc de registres physique, ne contenant que des registres flottants. Les architectures K8 et K10 utilisent le renommage de registres pour tous les registres, entiers comme flottants. Par contre, le renommage de registre n'est pas réalisé de la même manière pour les registres entiers et flottants. Les registres entiers sont renommés dans le tampon de ré-ordonnancement, comme c'était le cas sur les architectures Intel avant le Pentium 4. Par contre, les registres flottants sont renommés grâce à un banc de registre physique. Le K8 est donc un processeur au renommage hybride, qui utilise les deux solutions de renommage principales. A partir du K7, le CPU optimise la prise en charge des instructions ''load-up''. Les instructions ''load-op'' sont appelées des macro-opérations dans la terminologie d'AMD, et aussi d'Intel. L'idée est que les instructions ''load-up'' sont décodées en micro-opérations intermédiaires. Elles sont propagées dans le pipeline comme étant une seule micro-opération, jusqu'à l'étage d'émission. Lors de l'émission, les instructions ''load-up'' sont scindées en deux micro-opérations : la lecture de l'opérande, puis l'opération proprement dite. Faire ainsi économise des ressources et optimise le remplissage du tampon de ré-ordonnancement, des fenêtres d'instructions, des stations de réservation, etc. Le tampon de réordonnancement est combiné avec divers circuits en charge de l'exécution dans le désordre, dans ce qui s'appelle l'''instruction control unit''. Il contient de 72 à, 84 instructions, qui sont regroupées en groupes de 3. Là encore, comme pour le K5 et le K6, le tampon de réordonnancement tient compte de la sortie des décodeurs. Les décodeurs fournissent toujours trois micro-opérations par cycle, quitte à remplir les vides par des NOP. Le tampon de réordonnancement reçoit les micro-opérations, NOP inclus, par groupes de 3, et est structuré autour de ces triplets de micro-opération, y compris en interne. Pour ce qui est de l'unité mémoire, elle est précédée par une file de µops mémoire, qui émet les accès mémoire dans l'ordre du programme. Elle est souvent qualifiée de ''Load-Store Queue'', mais ce n'est pas la terminologie que nous utilisons dans ce cours. La file de micro-opération lire/écrire 64 bits par cycle depuis le cache L1, ce qui fait un seul accès au cache par cycle. La file de µops mémoire est appelée la ''Pre-Cache Queue''. Si au vu de son nom, vous avez deviné qu'il y avait une ''Post-Cache Queue''. Elle mémorise les lectures/écritures émises, mais qui ont levé un défaut de cache L1. Elle ne fait pas partie de la file de µops mémoire proprement dite. Les architectures K7, K8 et K10 ont des unités de calcul très similaires. Concrètement, il y a trois ALU entières, trois unités de calcul d'adresse, et une FPU. Le processeur incorpore, aussi un multiplieur entier, relié sur le port d'émission de la première ALU. La FPU regroupe un additionneur flottant, un multiplieur flottant, et une troisième unité LOAD/STORE pour les lectures/écritures pour les nombres flottants. L'architecture K8 ajoute une unité de manipulation de bit, la K10 un diviseur entier. [[File:Unité de calcul des processeurs AMD K7, K8 et K10.png|centre|vignette|upright=2|Unité de calcul des processeurs AMD K7, K8 et K10]] La manière d'alimenter les ALU en micro-opérations varie un petit peu entre les architectures K7, K8 et K10. Il y a cependant quelques constantes entre les trois. La première est qu'il y a une fenêtre d'instruction séparée pour les flottants, de 36 à 42 entrées, avec renommage de registre. La fenêtre d'instruction flottante a trois ports d'émission : un pour l'additionneur flottant, un autre pour le multiplieur, et un troisième pour la troisième unité flottante qui s'occupe du reste. La seconde est que chaque ALU entière est couplée avec une unité de calcul d'adresse. Par contre, la méthode de couplage varie d'un processeur à l'autre. : Les stations de réservation sont nommées des ''schedulers'' dans les schémas qui suivent. La microarchitecture K7 avait deux fenêtres d'instruction : une pour les opérations flottantes, une autre pour les instructions entières et les accès mémoire. La fenêtre d'instruction entière était reliée à 3 ALU entières et à 3 AGU. Elle pouvait émettre trois micro-opérations en même temps : trois micro-opérations entières, trois micro-opérations mémoire. Les AGU étaient reliées à la file de µops mémoire mentionnée plus haut, ce qui permet d'émettre trois µops mémoire par cycle. Par contre, la file de µops mémoire ne pouvait exécuter qu'une lecture de 64 bits ou une écriture de 64 bits. En clair, trois micro-opérations mémoire peuvent être émises par cycle, cela entraine trois calculs d'adresse simultanés, mais les trois lectures/écritures sont mises en attente dans la file de µops mémoire. Elles s'exécutent alors l'une après l'autre. La fenêtre d'instruction entière contenait 5 à 6 groupes de 3 macro-opérations. Vous noterez que j'ai parlé de macro-opérations et pas de micro-opérations, car les instructions ''load-up'' sont considérées comme une seule "micro-opération" dans la fenêtre d'instruction entière. Et cela se marie bien avec une fenêtre d'instruction unique partagée entre pipeline entier et pipeline mémoire. Une macro-opération était scindée en deux micro-opérations : une micro-opération mémoire et une micro-opération entière. Il est donc avantageux de regrouper unités mémoire et unités entières à la même fenêtre d'instruction pour ce faire. [[File:AMD K7.png|centre|vignette|upright=3|AMD K7]] Sur les architectures K8 et K10, la station de réservation unique de 15 micro-opérations est remplacée par trois stations de réservations, de 8 micro-opérations chacune pour le K8, de 10 pour le K10. Chaque station de réservation entière alimente une unité de calcul entière et une unité de calcul d'adresse. l'unité de calcul d'adresse est reliée à la file de µops mémoire, qui n’exécute toujours qu'un seul accès mémoire par cycle. Le multiplieur est relié à la première station de réservation, sur le même port d'émission que l'ALU. [[File:AMD Husky microarchitecture.png|centre|vignette|upright=3|AMD Husky microarchitecture]] La microarchitecture K10 a été déclinée en plusieurs versions, nommées Grayhound, Grayhound+ et Husky, Husky étant une architecture gravée en 32 nm dédiée aux processeurs A-3000. L'architecture Grayhound a plus de cache et un ROB plus grand, la Husky est quand à elle un peu plus différente. Elle n'a pas de cache L3, contrairement aux autres architectures K10, ce qui simplifie fortement son sous-système mémoire. Par contre, les fenêtres d'instructions/stations de réservation et le ROB sont plus grands, pareil pour les files dans l'unité mémoire. Une ALU pour les divisions entières a aussi été ajoutée. Pour résumer, les architectures K7, K8 et K10 séparent les pipelines entiers et flottants : trois pipelines entiers avec chacun son unité de calcul, et un pipeline flottant avec plusieurs unités de calcul. Les raisons à cela sont assez diverses. Disons que dupliquer des ALU entières simples prend peu de transistors, là où les gros circuits comme le multiplieur ou la FPU ne sont pas dupliqués. Et cela a un autre avantage : le renommage, ''dispatch'' et l'émission sont plus simples. Les pipelines entiers ont une exécution dans le désordre peu complexe, grâce au grand nombre d'unités de calcul, ce qui fait que le pipeline entier est de seulement 15 cycles au total (chargement et décodage inclus). A l'opposé, la FPU est alimentée par une exécution dans le désordre très complexe, avec banc de registre physique et beaucoup de ressources, mais au prix d'un pipeline flottant plus long de 3 cycles, soit 18 cycles au total. ===Les microarchitectures ZEN d'AMD=== Viennent ensuite les '''microarchitectures Bulldozer''', avec trois révisions ultérieures nommées Piledriver, Steamroller et Excavator. Mais du fait de l'utilisation de techniques de multithreading matériel que nous n'avons pas encore abordé, nous ne pouvons pas en parler ici. Les microarchitectures suivantes sont les '''architectures ZEN 1/2/3/4/5'''. Elles se ressemblent beaucoup, chacune accumulant les améliorations des précédentes. Mais le cœur de l'architecture reste plus ou moins le même. En passant à la suivante, le nombre de registre virtuel augmente, le ''branch target buffer'' augmente en taille, le ROB et les files d'attente grossissent, les caches de micro-opération aussi, les caches grossissent, etc. Une optimisation intéressante est l'ajout d'un cache de micro-opération, qui améliore grandement les performances du ''front-end'', notamment pour les boucles. La microarchitecture Zen 1 est illustrée ci-dessous. Comme on le voit, les registres flottants ont une unité de renommage séparée de celle pour les entiers, mais les deux utilisent du renommage à banc de registre physique. Il y a par contre une différence au niveau des fenêtres d'instruction, notées ''scheduler'' dans le schéma. Pour ce qui est des unités de calcul flottantes, il y a une fenêtre unifiée qui alimente quatre ALU, grâce à 4 ports d'émission. Mais pour les ALU entières, il y a une fenêtre d'instruction par ALU, avec un seul port d'émission connecté à une seule ALU. La raison de ce choix est que les opérations flottantes ont un nombre de cycle plus élevé, sans compter que les codes flottants mélangent bien additions et multiplication. Une fois décodées, les instructions sont placées dans une première file de micro-opérations om elles attendent, puis sont dispatchées soit dans le pipeline entier, soit dans le pipeline flottant. les micro-opérations entières sont insérées dans une fenêtre d'instruction directement, alors que les micro-opérations flottantes doivent patienter dans une seconde file de micro-opérations. La raison est que les micro-opérations flottantes ayant une grande latence, trop d'instructions flottantes consécutives pourraient bloquer le pipeline flottant, sa fenêtre d'instruction étant pleine. Le pipeline flottant étant bloqué, la première file de micro-opérations serait bloquée et on ne pourrait plus émettre de micro-opérations entières. Pour éviter cela, une solution serait d'agrandir la file de micro-opérations, mais cela la rendrait plus lente et se ferait donc au détriment de la fréquence d'horloge. Alors une solution a été d'ajouter une seconde file de micro-opérations, au lieu d'agrandir la première. [[File:Zen microarchitecture.svg|centre|vignette|upright=3|Microarchitecture Zen 1 d'AMD.]] Le passage à la microarchitecture n'a pas causé de grands changements. Le Zen 2 a ajouté une unité de calcul d'adresse, ce qui fait qu'on passe à 4 ALU, 3 AGU et 4 FPU. La fenêtre d'instruction flottante reste la même. Par contre, les fenêtres d'instruction entières changent un peu. Ou plutot devrais-je dire les fenêtres d'instruction mémoire. En effet, le Zen 2 fusionne les fenêtres d'instructions liées aux AGU en une seule fenêtre d'instruction deux fois plus grosse. Le Zen 5 a ajouté deux autres ALU entières et une unité de calcul d'adresse (6 ALU / 4 AGU) ==Les processeurs Atom d'Intel, de microarchitecture Bonnell== L'architecture de l'Atom première génération est assez simple. Son pipeline faisait 16 étages, ce qui est beaucoup. C'est un processeur 32 bits, ce qui aura son importance dans ce qui suit. Il était conçu pour être un processeur basse consommation, donc peu puissant. En conséquence, il n'a pas d'exécution dans le désordre, même s'il est superscalaire. C'était la norme à l'époque pour les processeurs basse consommation, que de faire sans exécution dans le désordre. De nos jours, les choses ont bien changée, même les processeurs basse consommation ont exécution dans le désordre, superscalarité et renommage de registres. ===Le ''front-end'' de l'Atom=== Le cache d'instruction permet de lire 8 octets par cycle, qui sont placés dans une file d'instruction, elle-même suivie par deux décodeurs. Le fait que les décodeurs lisent les instructions depuis une file d'instruction fait que les deux instructions décodées ne sont pas forcément consécutives en mémoire RAM. Par exemple, l'Atom peut décoder un branchement prédit comme pris, suivi par l'instruction de destination du branchement. Les deux instructions ont été chargées dans la file d'instruction et sont consécutifs dedans, alors qu'elles ne sont pas consécutives en mémoire RAM. Sur l'Atom, la majorité des instructions x86 sont décodées en une seule micro-opération, y compris les instructions ''load-up''. Le microcode n'est utilisé que pour une extrême minorité d'instructions et est à part des deux décodeurs précédents. L'avantage est que cela permet d'utiliser au mieux la file de micro-opération, qui est de petite taille. Mais surtout, cela permet de grandement réduire la consommation du processeur, au détriment de ses performances. Pour avoir un décodage rapide, malgré des instructions complexes, le processeur recourt à la technique du pré-décodage, qui prédécode les instructions lors de leur chargement dans le cache d'instruction. Le prédécodage lui-même prend deux cycles, là où une lecture dans le L1 d'instruction en prend 3. les défauts de cache d'instruction sont donc plus longs de deux cycles. Mais l'avantage du prédécodage est que la consommation d'énergie est diminuée. Prenez une instruction exécutée plusieurs fois, dans une boucle. Au lieu de décoder intégralement une instruction à chaque fois qu'on l'exécute, on la prédécode une fois, seul le reste du décodage est fait à chaque exécution. D'où un gain d'énergie assez intéressant. Les caches de micro-opération, qui sont capables d'exécuter une optimisation similaire, n'existaient pas encore à cette époque. ===Le chemin de données de l'Atom=== Les deux décodeurs alimentent une file de micro-opérations de petite taille : 32 µops maximum, 16 par ''thread'' si le ''multithreading'' matériel est activé. La file de micro-opérations a deux ports d'émission, ce qui permet d'émettre au maximum 2 µops par cycle. Les conditions pour cela sont cependant drastiques. Les deux instructions ne doivent pas avoir de dépendances de registres, à quelques exceptions près liées au registre d'état. Le multithreading matériel doit aussi être désactivé. Les deux instructions doivent aller chacun dans un port différent, et cela tient en compte du fait que les deux ports sont reliés à des unités de calcul fort différentes. Le tout est illustré ci-dessous. Les deux ports ont chacun une ALU simple dédiée, capable de faire des additions/soustractions, des opérations bit à bit et des copies entre registres. Mais ils ont aussi des opérations qui leur sont spécifiques. La séparation entre les deux pipelines est assez complexe. Il ne s'agit pas du cas simple avec un pipeline entier et un pipeline flottant séparés. En réalité, il y a deux pipelines, chacun capables de faire des opérations entières et flottantes, mais pas les mêmes opérations. Le premier port permet d’exécuter des opérations entières simples, une addition flottante, des comparaisons/branchements, ou une instruction de calcul d'adresse LEA. Le second port/pipeline est, quant à lui, conçu pour exécuter les instruction ''load-up'' nativement, en une seule micro-opération. Il contient toute la machinerie pour faire les accès mémoire, notamment des unités de calcul d'adresse et un cache L1 de données. A la suite du cache, se trouvent une ALU entière simple, un ''barrel shifter'', et un circuit multiplieur/diviseur. Le circuit multiplieur/diviseur est utilisé à la fois pour les opérations flottantes et entières. [[File:Intel Atom Microarchitecture.png|centre|vignette|upright=2.5|Intel Atom Microarchitecture]] Cette organisation difficile à comprendre est en réalité très efficace, très économe en circuit, tout en gardant une performance intéressante. Les instructions simples, ADD/SUB/bitwise sont supportées dans les deux pipelines. Il faut dire que ce sont des opérations courantes qu'il vaut mieux optimiser au mieux. Le processeur peut donc émettre deux opérations simples et fréquentes en même temps, ce qui augmente les performances. Les opérations plus complexes, à savoir les multiplications/divisions/décalages/rotations/manipulations de bit sont supportées dans un seul pipeline. La raison est qu'il est rare que de telles opérations soient consécutives, et qu'il n'est donc pas utile d'optimiser pour cette situation. Si les deux pipelines devaient supporter ces opérations, cela demanderait de dupliquer les circuits multiplieurs/diviseur, ce qui aurait un cout en circuit important pour un gain en performance assez faible. ===Le système d'exceptions flottantes de l'Atom=== Le processeur étant sans exécution dans le désordre, ses instructions doivent écrire dans les registres dans l'ordre du programme. En conséquence, certaines instructions doivent être retardées, leur émission doit attendre que les conditions soient adéquates. Et cela pose problème avec les opérations flottantes, vu qu'elles prennent pas mal de cycles pour s'exécuter. Imaginez qu'une instruction flottante de 10 cycles soit suivie par une instruction entière. En théorie, on doit retarder l'émission de l'instruction entière de 9 cycles pour éviter tout problèmes. Le cout en performance est donc assez important. En théorie, les instructions entières et flottantes écrivant dans des registres séparés, ce qui fait que l'on pourrait exécuter instructions entières et flottantes dans le désordre. Sauf pour les instructions de copie entre registres entier et flottants, mais laissons-les de côté. Le problème est qu'une instruction flottante peut parfois lever une exception, par exemple en cas de division par zéro, ou pour certains calculs précis. Si une exception est levée, alors l'instruction flottante est annulée, de même que toutes les instructions qui suivent, y compris les opérations entières. Ce n'est pas un problème si le processeur gère nativement les exceptions précises, par exemple avec un tampon de ré-ordonnancement. Mais l'Atom étant un processeur sans exécution dans le désordre, les instructions entières devraient être mises en attente tant qu'une instruction flottante est en cours d'exécution. Heureusement, l'Atom d'Intel a trouvé une parade. La technique, appelée ''Safe Instruction Recognition'' par Intel, est décrite dans le brevet US00525721.6A. L'idée est de tester les opérandes flottantes, pour détecter les combinaisons d'opérandes à problème, dont l'addition/multiplication peut lever une exception. Si des opérandes à problème sont détectées, on stoppe l'émission de nouvelles instructions en parallèle de l'instruction flottante et l'unité d'émission émet des bulles de pipeline tant que l'instruction flottante est en cours. Sinon, l'émission multiple fonctionne. La technique permet ainsi de ne pas écrire dans les registres entiers/flottants dans l'ordre du programme : une instruction entière peut être autorisée à s'exécuter même si elle écrit dans un registre entier avant qu'une instruction flottante délivre son résultat. ==La microarchitecture Netburst du Pentium 4== Dans cette section, nous allons voir l'architecture du processeur Pentium 4, qu'on a volontairement laissée de côté précédemment. Pourquoi un tel saut dans le temps ? Parce que le Pentium est complément à part des autres architectures Intel. Le Pentium 4 a représenté une rupture en termes de microarchitecture, qui a été un échec tellement retentissant que les processeurs suivants sont repartis sur la base du Pentium 3. Il introduisait de nombreuses nouveautés architecturales qui étaient très innovantes. Par exemple, il introduisait le renommage avec un banc de registre physique, qui a été utilisé sur tous les processeurs Intel suivants. Mais la plupart de ces innovations étaient en réalité de fausses bonnes idées, ou du moins des idées difficiles à exploiter. Par exemple, le système de pipeline à ''replay'' n'a été utilisé que sur le Pentium 4 et aucun autre processeur ne l'a implémenté. ===Un focus sur la fréquence d'horloge=== La microarchitecture du Pentium 4 a été déclinée en plusieurs versions, dont les finesses de gravure n'étaient pas les mêmes. La microarchitecture Netburst, utilisée sur le Pentium 4, utilisait un pipeline à 20 étage, augmenté à 32 sur une révision ultérieure. Il a existé quatre révisions de l'architecture : Willamette (180 nm), Northwood (130 nm), Prescott (90 nm) et Cedar Mill (65 nm). Un point important est que le Pentium 4 était prévu pour fonctionner à haute fréquence. Ses 1,5 GHz étaient impressionnants pour l'époque, les autres processeurs tournant à une fréquence proche du GigaHertzs. Pour cela, la solution retenue par Intel a été un pipeline très long, avec beaucoup d'étages. Le Pentium 4 a été décliné en plusieurs versions assez proches, chacune avec sa propre finesse de gravure qui n'ont pas toute le même pipeline. Les micro-architectures ''Willamette'' et ''Northwood'' avaient un pipeline de 20 étages, alors que les autres processeurs de l'époque avaient entre 10 et 15 étages maximum. Les micro-architectures ''Prescott'' et ''Cedar Mill'' étaient une refonte qui a fait grimper le nombre d'étages à 31 ! Du jamais vu, il s'agit d'un record pour un processeur commercial. Un pipeline aussi long permet d'exécuter beaucoup d’instructions en même temps, chacune dans un étage, mais aussi d'atteindre de hautes fréquences facilement. Le problème est qu'un pipeline avec autant d'étages a beaucoup de problèmes. Un point important est que la prédiction de branchement est cruciale. Pour rappel, la pénalité en cas de mauvaise prédiction dépend du nombre d'étages avant que le branchement soit résolu. Et les branchements sont résolus soit en fin de décodage, soit dans l'unité de calcul. C'est à dire au milieu du pipeline, soit en fin de pipeline. La pénalité en cas de mauvaise prédiction de branchement était énorme sur le Pentium 4, elle atteignait facilement 30 cycles Pour compenser, le Pentium 4 avait une prédiction de branchement très performante, pour l'époque. J'insiste sur le pour l'époque. Il utilisait un prédicteur qu'on a déjà abordé dans le chapitre sur la prédiction de branchement, précisément un prédicteur adaptatif à deux niveaux avec un historique global de 16 bits. Il avait aussi un ''Branch Target Buffer'' de 4096 entrées. Mais surtout, il intégrait une sorte de précurseur du cache de micro-opération, appelé le cache de traces, qui est détaillé dans la section suivante. ===Le cache de trace du Pentium 4=== Les décodeurs du Pentium 4 ne font pas décoder les instructions, ils mémorisent le résultat dans un cache de micro-opération un peu particulier, appelé le '''cache de trace'''. Une ligne de cache peut mémoriser 6 micro-opérations, ce qui peu sembler peu mais a été repris sur les micro-architectures suivantes. Mais le cache de trace a une grande différence avec un cache de micro-opération normal. Un cache de micro-opération normal mémorise une instruction par ligne de cache. Une instruction est décodée en plusieurs micro-instructions, qui sont enregistrées dans une ligne de cache. Si l'instruction n'utilise par les 6 micro-opérations disponibles, le reste de la ligne de cache n'est pas utilisé. Mais le Pentium 4 optimise le tout de manière ce à ce que ne soit pas le cas. Sur le Pentium 4, la contrainte du "une instruction par ligne de cache" est abandonnée. Une ligne de cache mémorise 6 micro-opérations consécutives, qui peuvent appartenir à plusieurs instructions. Par exemple, si le décodeur décode 4 instructions consécutives en 6 micro-opérations au total, alors le tout prendra une seule ligne de cache sur le Pentium 4. Et les 4 instructions consécutives n'ont même pas à être consécutives en mémoire : il peut y avoir des branchements pris entre ces instructions ! {|class="wikitable" |+ Cache de trace |- ! Ligne de cache | ADD || SUB || ADD || MOV || MUL || ''shift'' |- ! Ligne de cache | colspan="3" | ADD ''load-up'' || MUL || colspan="2" | Branch if Equal |- ! Ligne de cache | XOR || colspan="4" | POP || SUB |- ! ... | colspan="6" | ... |} Pour expliquer cela plus concrètement, nous allons devoir introduire les concepts de trace et de bloc de base. Un '''bloc de base''' (''basic block'') est une suite d'instructions sans branchement, qui est séparé par deux branchements. Le début d'un bloc de base est la destination d'un branchement, un bloc de base se termine avec un branchement. Une '''trace''' est formée en concaténant plusieurs blocs de base. Pour donner un exemple, regardez le code illustré ci-contre. Il est composé d'un bloc de base A, suivi par un bloc de base B, qui peut faire appel soit au bloc C, soit un bloc D. Un tel code peut donner deux traces : ABC ou ABD. La trace exécutée dépend du résultat du branchement qui choisit entre C et D. Le cache de trace mémorise des traces de 6 micro-opérations consécutives. Les traces sont formées en sortie des décodeurs d'instruction, par de subtiles opérations mélangeant mémorisation, décalage et concaténation. Les circuits qui construisent les traces ne sont pas connus, mais ils doivent certainement être très compliqués. toujours est-il qu'un cache de trace peut mémoriser des traces différentes, même si leur début est le même. Par exemple, prenons deux traces, composées des blocs de base A, B, C et D. La première trace est la trace ABC, la seconde est la trace ABD. Les deux traces auront chacune une ligne de cache dédiée. Une trace est réutilisable quand le premier bloc de base est identique et que les prédictions de branchement restent identiques. Pour vérifier cela, le tag du cache de traces contient l'adresse du premier bloc de base, la position des branchements dans la trace et le résultat des prédictions utilisées pour construire la trace. Le résultat des prédictions de branchement de la trace est stocké sous la forme d'une suite de bits : si la trace contient n branchements, le n-ième bit vaut 1 si ce branchement a été pris, et 0 sinon. Même chose pour la position des branchements dans la trace : le bit numéro n indique si la n-ième instruction de la trace est un branchement : si c'est le cas, il vaut 1, et 0 sinon. Si la trace est réutilisée par la suite, elle est lue depuis le cache de traces. Pour savoir si une trace est réutilisable, l'unité de chargement envoie le ''program counter'' au cache de traces, l'unité de prédiction de branchement fournit le reste des informations. Si on a un succès de cache de traces, et la trace est envoyée directement au décodeur. Sinon, la trace est chargée depuis le cache d'instructions et assemblée. Il faut signaler que le cache de trace avait sa propre unité de prédiction de branchement séparée de l'unité de prédiction de branchement normale. [[File:TraceCache.png|centre|vignette|upright=2|Cache de traces.]] Le cache de traces réduisait la longueur du pipeline en cas de succès de cache de trace. Quand les instructions étaient lues depuis le cache de trace, les étages avant le cache de trace ne sont pas utilisés, tout se passe comme s'ils étaient retirés du pipeline. C'est la même chose avec le cache de micro-opération des processeurs modernes, mais l'idée n'existait pas encore à l'époque. Le cache de trace mémorise des traces décodées, ce qui fait qu'un succès de cache de trace contournait non seulement le cache d'instruction, mais aussi les décodeurs. Le temps d'accès au cache de trace pouvait être assez élevé, même s'il était comparable au temps d'accès du cache d'instruction. Le cache de traces a depuis été remplacé par une alternative bien plus intéressante, le cache de micro-opérations, plus flexible et plus performant. Comparé à un cache de trace, la contrainte "une instruction par ligne de cache" simplifie grandement l'implémentation d'un cache de micro-opération. Ne parlons pas de la détection des succès de cache, qui demande d'utiliser les prédictions de branchement. Mais le vrai problème avec le cache de trace est tout autre. Il arrive souvent qu'une micro-opération soit présente dans plusieurs lignes de cache en raison du processus de construction des traces, chose impossible avec un cache de micro-opération. Et c'est un problème, qui réduit la capacité effective du cache de trace. Alors certes, une ligne de cache est plus remplie que sur un cache de micro-opération, on est certain que les 6 micro-opération par ligne de cache sont remplies. Mais la redondance réduit grandement cet avantage. ===L'exécution dans le désordre et le chemin de données du P4=== Le renommage de registres se fait avec un banc de registres physiques avec une table d'alias. Le Pentium 4 avait une exécution dans le désordre très limitée, basée sur la présence de deux files de micro-opération : une pour les accès mémoire, une autre pour les autres instructions. La seconde file regroupait opérations entières et flottantes, elles n'étaient pas séparées. Avec ces deux files, les instructions mémoire étaient exécutées dans l'ordre du programme, les instructions arithmétiques s'exécutaient aussi dans l'ordre du programme, mais une instruction arithmétique pouvait passer avant une instruction mémoire et inversement. L'avantage est que cela permettait de faire des lectures en avance, c'était une forme limitée de lecture non-bloquantes. Il s'agit bel et bien de deux files d'instructions, pas de fenêtres d'instruction ni de stations de réservation. Le Pentium 4 est le seul processeur commercial qui a utilisé des files de micro-opération séparées, tous les autres utilisent des fenêtres d'instruction : centralisées pour Intel, décentralisées pour AMD (en général). Le Pentium 2 et 3, bien qu'antérieurs, utilisait une station de réservation unique. Cela peut sembler être un retour en arrière, mais les files d'instructions sont bien plus larges : de 42 micro-opérations pour le Pentium 3, on passe à 120 micro-opérations pour le Pentium 4. Et vu la longueur du pipeline, qui fait qu'il y a plus d'instructions en vol, c'était une nécessité. Mais cela n'aurait pas été possible en utilisant des stations de réservation, pour des raisons de consommation électrique et/ou de budget en transistors, ce qui fait que passer à une file de micro-opération été la solution retenue. Pour les accès mémoire, le Pentium 4 utilisait donc une file de µops mémoire unique, couplée à une file d'écriture (non-représentée sur les schémas qui suivent). La file d'écriture du Pentium 4 était de 24 écritures maximum et gérait le ''Store-to-load forwarding''. Le processeur pouvait émettre une lecture et une écriture à chaque cycle. Il y avait un port d'émission pour les lectures et un autre pour les écritures, tous deux ayant chacun leur propre unité de calcul d'adresse. Le processeur contenait 3 ALU entières, 2 unités de calcul d'adresse et une FPU. La FPU était complétée par une unité pour faire des copies entre registres flottants, des opérations MOV. Pour les AGU, il y en avait une dédiée aux lectures, une autre pour les écritures. Le tout était relié aux ports d'émissions comme suit : [[File:Ports d'émission du Pentium 4.png|centre|vignette|upright=2.5|Ports d'émission du Pentium 4]] Le processeur utilisait deux réseaux de contournement séparés : un pour les opérations flottantes, un pour les opérations entières. Le réseau de contournement pour les opérations entières est aussi relié aux unités de calcul d'adresse. Jusque là, rien de surprenant, le chemin de données du processeur est assez classique. [[File:Architettura Pentium 4.png|centre|vignette|upright=3|Microarchitecture du Pentium 4.]] ===Les unités de calcul entières du Pentium 4=== Sur le Pentium 4, les ALU entières étaient cadencées à une fréquence double de celle du processeur. Les ALU entières pouvaient exécuter deux micro-opérations par cycle, ce qui fait que les ports d'émissions reliés aux ALU devaient eux aussi fonctionner à double fréquence. Pour faire la différence entre les deux fréquences, nous parlerons de fréquence/cycle processeur et de fréquence/cycle de l'ALU. Précisons que seules les ALU entières étaient à double fréquence, pas le multiplieur, pas le ''barrel shifter''. Pour simplifier, nous allons parler d'additionneur plutôt que de l'ALU entière, ce qui sera plus proche de la réalité. Et l'implémentation de l'additionneur du Pentium 4 était très innovante. L'additionneur pouvait exécuter deux additions par cycle, même si les deux additions ont une dépendance. Mais n'allez pas croire que l'implémentation était intuitive, avec un additionneur 32 bit basique très rapide. Non seulement l'additionneur fonctionnait à double fréquence, mais il était aussi pipeliné, avec un système de contournement interne ! Les additionneurs étaient pipelinées, d'une manière très simple. Une addition 32 bits était découpée en trois étapes : deux additions de 16 bits, une dernière étape pour mettre à jour le registre d'état. Pour cela, chaque additionneur était composé de deux additionneur 16 bits chacune, placées l'une après l'autre, avec un registre de pipeline entre les deux. L'additionneur prenait deux cycles d'horloge pour faire son travail : le premier cycle calculait les 16 bits de poids faible, le second calculait les 16 bits de poids fort lors du second cycle. Le tout est appelé '''addition étagée''' (''staggered add'') dans la documentation Intel. Une addition se fait donc en deux étapes, sauf que c'est compensé par le fait que l'additionneur fonctionnait à une fréquence double de celle du processeur ! Le résultat de ce fonctionnement franchement bizarre, est que les 16 bits de poids faible étaient calculés en une moitié de cycle processeur, alors que l'opération complète prenait un cycle. Deux additions consécutives s'exécutaient donc en 1 cycle et demi, alors qu'on aurait cru au premier abord que cela prendrait seulement un cycle. Si on fait les calculs, on s'apercoit que le rythme de croisière est cependant proche de 2 additions par cycle, bien qu'inférieur. 3 additions consécutives se font en deux cycles, 5 additions en 3 cycles, 7 en 4 cycles, etc. Et le Pentium 4 ajoutait un système de contournement interne à l'ALU. En clair, si une addition utilise le résultat de l'addition précédente, les deux peuvent s'exécuter en un cycle d'horloge et demi. Les 16 bits de poids faible de la première addition sont disponibles après un cycle ALU, ce qui permet de démarrer le calcul de la seconde addition au cycle suivant. ===Le ''replay pipeline''=== Le processeur est un processeur triple émission : il peut charger et décoder 3 µops par cycle, le ROB peut terminer 3 µops par cycle, etc. Pourtant, les ports d'émission peuvent émettre 6 instructions par cycle : 4 ports, dont deux à double fréquence. Une telle différence s'explique par l'usage d'un ''replay pipeline'', dont nous avons déjà parlé dans ce cours. Pour rappel, le Pentium 4 suppose que les lectures font tous un succès de cache L1. Si une opération arithmétique utilise la donnée lue comme opérande, le processeur l'émet immédiatement, l'opérande sera disponible une fois l'opération en entrée de l'ALU entière. Mais s'il s'est trompé, le processeur ré-exécute l'instruction après un temps d'attente de quelques cycles, pour se caler sur la latence du cache L2. Et si il y a un défaut de cache L2, l’instruction attend encore. Cela demande de ré-exécuter des instructions émises à tord, ce qui fait que le processeur doit avoir la capacité d'exécution pour. Ce pourquoi le processeur peut émettre 6 µops dans les unités de calcul : 3 µops normales et 3 µops ré-exécutées. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Les processeurs superscalaires | prevText=Les processeurs superscalaires | next=Les processeurs VLIW et EPIC | nextText=Les processeurs VLIW et EPIC }} </noinclude> ouodg89mdtpbhgc07b1l5jzqgwitsy6 772756 772754 2026-09-21T21:23:56Z Mewtow 31375 /* Les microarchitectures récentes d'Intel */ 772756 wikitext text/x-wiki Dans ce chapitre, nous allons étudier des exemples de processeurs x86, ceux présents dans nos PC. Nous n'allons pas voir les anciens processeurs comme le 286, le 386 ou le 486. Nous allons commencer avec le Pentium 1, et les processeurs commerciaux qui ont suivis. Tous les processeurs que nous allons voir dans ce chapitre sont des processeurs superscalaires. De fait, ce n'est pas pour rien si ce chapitre se situe après le chapitre sur les processeurs superscalaires. Par contre, nous allons voir que certains n'ont pas d'exécution dans le désordre. La raison est que l'exécution dans le désordre est arrivé après la superscalarité. ==Généralités sur les CPU x86 superscalaires== Avant de voir chaque processeur indépendamment des autres, nous allons devoir aborder quelques généralités. Nous allons d'abord voir que le jeu d'instruction x86 pose quelques problèmes pour la superscalarité. Et ces problèmes posent des contraintes assez fortes, avec lesquelles les concepteurs de processeurs dovient faire avec. Nous poursuivrons ensuite par un historique des processeurs Intel et AMD, histoire de donner un peu de contexte aux processeurs que nous allons étudier. ===Le jeu d'instruction x86 pose des problèmes pour la superscalarité=== Une difficulté de l'architecture x86 est qu'il s'agit d'une architecture CISC, avec tous les défauts que ça implique. Un jeu d'instruction CISC a en effet de nombreuses propriétés qui collent mal avec l'émission multiple, avec la '''superscalarité'''. Il y en a plusieurs, certaines impactent le chargement des instructions, d'autres leur décodage, d'autres l'exécution, etc. Premièrement, les instructions sont de longueur variable, entre 1 et 15 octets, ce qui complique leur chargement et leur décodage. En pratique, les processeurs chargent un bloc de 32 à 64 octets, et découpent celui-ci en plusieurs instructions. La conséquence est que l'usage d'instructions trop longues peut poser problème. Imaginez qu'un processeur charge un bloc de 16 octets et que celui-ci ne contienne qu'une seule instruction : on ne profite pas de la superscalarité. Deuxièmement, une partie des instructions est microcodée, faute de mieux. Et cela pose de sérieux challenges pour l'implémentation des décodeurs. Dupliquer le microcode demanderait trop de transistors, ce qui fait que ce n'est pas fait. À la place, il n'y a qu'un seul microcode, ce qui fait que l'on ne peut pas décoder plusieurs instructions microcodées en même temps. Il est cependant possible de profiter de la superscalarité, en décodant une instruction microcodée en parallèle d'autres instructions non-microcodées. Et heureusement, ce cas est de loin le plus fréquent, il est rare que plusieurs instructions microcodées se suivent. Troisièmement, la présence d'instructions ''load-up'', qui lisent un opérande en mémoire, peut poser problème, mais est aussi source d'optimisations assez intéressantes. En théorie, une instruction ''load-op'' est décodée en deux micro-opération : une pour lire d'opérande en RAM, l'autre pour faire l'opération arithmétique. Sauf que les processeurs x86 modernes optimisent la gestion des instructions ''load-up''. Par exemple, les premiers processeurs Atom géraient des micro-opérations de type ''load-up'', directement dans le chemin de données ! D'autres processeurs utilisent la technique de la '''micro-fusion''' pour retarder le décodage réel des instructions ''load-up'' assez loin dans le pipeline. Avec eux, une instruction ''load-op'' est décodée en une seule "macro-opération", qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. L'avantage est qu'une macro-opération ne prend qu'une seule entrée dans le tampon de ré-ordonnancement, la fenêtre d'instruction, la file de micro-opération, et les autres structures similaires. ===Un petit historique des processeurs x86 superscalaires=== Nous allons voir les processeurs Intel à part des processeurs AMD. La raison à cela est que les architectures Intel et AMD ont progressivement évolué, chacune se basant sur la précédente et l'améliorant. Il n'y a pas eu de cassure entre microarchitectures AMD, qui sont chacune la suite de la précédente. Il est donc préférable de voir les architectures AMD dans l'ordre chronologique. Par contre, Intel a eu une gigantesque cassure, avec le processeur Pentium 4. Son architecture se démarquait fortement du Pentium 3, mais elle n'a pas convaincu et a été abandonnée avec les processeurs suivants. Ce qui fait nous verrons l'architecture du Pentium 4 à part. Le Pentium 1 et 2 utilisaient la même architecture, qu'on détaillera dans ce qui suit. Les seules différences importantes étaient la fréquence, le cache, et quelques détails dans le genre. Le Pentium 3 était une nouvelle microarchitecture qui ajoutait l'exécution dans le désordre. Un an et demi plus tard, le Pentium 4 est sorti et a été un échec. Ses performances étaient peu convaincantes face au Pentium 3, et sa consommation énergétique était très importante. La conséquence est que le Pentium 4 et le Pentium 3 ont survécu pendant un long moment, beaucoup de monde préférait acheter un Pentium 3. Intel a alors amélioré les microarchitectures du Pentium 3 et du 4, indépendamment, pendant environ 7 ans. La microarchitecture du Pentium 3 a subit plusieurs micro-évolutions, chacune avec une finesse de gravure différente, afin de satisfaire les consomateurs. Les premeirs Pentium 3 avaient une finesse de gravure de 250 nm, elle a chuté à 65 sur les derniers modèles. L'architecture du Pentium 4 a fait la même chose, pour tenter de corriger ses problèmes de performance et de consommation d'énergie. Les premiers Pentium 4 avaient uen finesse de gravure de 180 nm, elle a elle aussi chutée à 65 sur les derniers modèles. Après l'échec du Pentium 4, les ingénieurs d'Intel ont repris l'architecture P6 et l'ont améliorée fortement, pour donner l'architecture Core. Les micro-processeurs suivants ont fait évoluer cette architecture progressivement, au point où elle ne ressemble plus à l'originale. L'architecture Core a laissé la place à l'architecture Nehalem, puis Sandy Bridge, puis Haswell, puis Skylake, puis Ice Lake, et Golden Cove. Il s'agit de la lignée principale, partant du Pentium 3 et continuant jusqu'à nos jours. Ces microarchitectures ont suivi un motif assez simple, appelé modèle '''tick-tock'''. Chaque microarchitecture était déclinée en deux versions, la seconde ayant une finesse de gravure réduite. En parallèle, Intel a travaillé sur des processeurs basse performance et basse consommation, avec une microarchitecture très différente. Les processeurs Atom de microarchitecture Bonnel, pour être ensuite remplacés par les microarchitectures Silvermont, puis Goldmont et Gracemont. Ces microarchitectures ont évolué en parallèle de la lignée principale, il s'agit d'une lignée secondaire. Le tout est résumé dans ce schéma ci-dessous. [[File:IntelProcessorRoadmap-4v.svg|centre|vignette|upright=2.5|Roadmap des processeurs Intel, qui servira de structure pour la suite du chapitre]] De nos jours, les processeurs Intel utilisent les deux types de micro-architectures en même temps. Les CPU Intel modernes disposent de deux types de coeurs : les coeurs P et le coeurs E. Leur nom signifie "Performance" et "Efficient", qui trahissent leur but. Les coeurs P utilisent la microarchitecture de la lignée principale, qui est conçue pour la performance. Les coeurs E, quant à eux, utilisent les micro-architectures basse consommation de la lignée secondaire. ==Les processeurs x86 d'Intel, la lignée principale== Pour commencer, nous allons voir les processeurs Intel. N'y voyez pas du favoritisme derrière ce choix, la justification est toute autre. Si je commence par Intel, c'est pour commencer avec les Pentium 1 et 2, qui sont plus simples que tous les autres. Il s'agit en effet de processeurs superscalaires, mais sans exécution dans le désordre. L’absence d'exécution dans le désordre les rend bien plus simples à étudier que les autres, ce qui en fait un bon point de départ pour ce chapitre. Le successeur du Pentium 2 a intégré l'exécution dans le désordre, ce qui fait que les Pentium 1 et 2 sont les seuls processeurs superscalaires ''in-order''. Pour la concurrence, AMD n'a pas produit de processeurs ''in-order'', tous les processeurs produits par AMD intègrent l'exécution dans le désordre. A la rigueur, il y a bien les processeurs Atom de la lignée secondaire d'Intel. Cependant, même s'ils sont bien des CPU ''in-order'', leur architecture est assez compliquée. En comparaison, le Pentium est une vieille architecture, qui se débrouillait avec peu de transistors et était donc bien plus simple que celle de l'Atom. ===Le Pentium 1/MMX et les pipelines U/V=== Le processeur Pentium d'Intel avait un pipeline de 5 étages : un étage de chargement/prédiction de branchement, deux étages de décodage, un étage d'exécution et un dernier étage pour l'écriture dans les registres. Le Pentium 1 était un processeur double émission, intégrant deux pipelines nommés U et V. Chose importante, les deux pipelines n'étaient pas identiques. Le pipeline U pouvait exécuter toutes les instructions, mais le pipeline V était beaucoup plus limité. Par exemple, seul le pipeline U peut faire des calculs flottants, le pipeline V ne fait que des calculs entiers et des branchements. Les deux pipelines disposaient d'une unité de calcul entière, identique dans les deux pipelines. Mais le pipeline U incorporait un circuit multiplieur/diviseur et d'un ''barrel shifter''. L'unité flottante était sur le port d'émission du pipeline U, idem pour l'unité de calcul vectoriel MMX sur le Pentium MMX. Les deux pipelines avaient chacun une unité de calcul d'adresse, mais ils n'étaient pas identiques : celle du pipeline V ne gérait que l’instruction LEA, celle du pipeline U gérait tous les calculs d'adresse. {|class="wikitable" |- ! Pipeline U ! Pipeline V |- | ALU entière | ALU entière |- | Multiplieur/diviseur | |- | ''Barrel Shifter'' | |- | AGU complexe | AGU simple (opération LEA) |- | FPU | |- | Unité SIMD | |} Les deux pipelines géraient les opérations bit à bit, les additions, les soustractions et les comparaisons. Les autres instructions ne sont exécutables que dans le pipeline U. Pour être plus précis, les deux pipelines supportaient les instructions suivantes, ce qui fait qu'on pouvait en faire deux en même temps : * Les instructions arithmétiques INC, DEC, ADD, SUB ; * l'instruction de comparaison CMP ; * les instructions bit à bit AND, OR, XOR ; * l'instruction de calcul d'adresse LEA ; * l'instruction MOV (dépend du mode d'adressage) ; * les instructions de gestion de la pile PUSH et POP (dépend du mode d'adressage) ; * l'instruction NOP, qui ne fait rien. Il faut noter qu'il y a cependant quelques restrictions, beaucoup de paires d'instructions sont interdites. La plupart interdisent au pipeline V de faire quoique ce soit quand une opération particulière est émise dans le pipeline U. Par exemple, si le pipeline U exécute une multiplication ou une division, le processeur ne peut pas exécuter une opération dans le pipeline V. Et c'est pareil avec les branchements : si un branchement est émis dans le pipeline U, l'instruction suivant le branchement n'est pas émise dans le pipeline V, pour éliminer les dépendances de contrôle. De même, si le pipeline U exécute une opération flottante, le pipeline V ne pourra rien exécuter. La seule exception est l'instruction FCXH, qui échange deux registres flottants. [[File:Intel Pentium arch.svg|centre|vignette|upright=2.5|Microarchitecture de l'Intel Pentium MMX. On voit que certaines unités de calcul sont dupliquées.]] Un choix assez intéressant a été fait pour le cache de données. Nous avions vu dans le chapitre sur les CPU superscalaires que la superscalarité a un impact sur l'unité mémoire. Il y a alors deux implémentations. La première ne fait rien, l'unité mémoire ne change pas, et le processeur ne peut pas faire deux accès mémoire simultanés. La seconde duplique l'unité mémoire et les ports de lecture/écriture du cache, ce qui autorise des accès mémoire simultanés. Le Pentium 1 utilise une solution intermédiaire. Les ingénieurs d'Intel étaient partis à la base sur un cache totalement double port, pour obtenir des performances maximales. Mais diverses simulations et observations les ont fait changer d'avis. Les simulations ont montré qu'il est "rare" que les deux pipelines aient besoin de lire/écrire dans le cache en même temps. Et ils ont optimisé le cache de donnée pour en tenir compte. Le cache de données est partiellement multiport : simple port sur certains aspects, double port sur d'autres. Le cache est un cache splité, à savoir que les données et les ''tags'' sont séparés dans des mémoires séparées. La mémoire pour les ''tags'' est multiport, ce qui permet d'interroger les ''tags'' du cache deux fois par cycle. Un port est relié au pipeline u, un autre au pipeline V. Mais pour les données, le cache n'a qu'un seul port pour lire/écrire des données. Impossible donc de lire deux données en même temps, pour alimenter les deux pipelines. Il utilise cependant 8 banques permet d'accélérer des accès mémoire proches dans le temps, mais dans des cycles d'horloge différents. Entre les deux mémoires, il y a un circuit qui détecte les conflits, à savoir les situations où les deux pipelines accèdent en même temps au cache. S'ils veulent lire/écrire une donnée dans le même cycle, ce qui est impossible avec un seul port, le pipeline U a la priorité et le pipeline V attend le cycle suivant. Le circuit détecte aussi les conflits de banque, à savoir quand un pipeline accède à une banque en cours d'accès par l'autre pipeline (on rappelle qu'un accès au cache prend plusieurs cycles). Le circuit détecte aussi certaines dépendances mémoires, à savoir des accès consécutifs à la même adresse. : La TLB du processeur est aussi totalement double port. ===La microarchitecture P6 du Pentium 2/3=== Le Pentium 3 utilisait la '''microarchitecture P6''', qui a été dérivée dans de nombreuses variantes, dont les finesses de gravure n'étaient pas les mêmes. Il introduit une exécution dans le désordre simple, avec une fenêtre d'instruction centralisée, avec renommage dans le désordre dans le ROB (tampon de ré-ordonnancement), commandé par une table d'alias. C'était un processeur triple émission, soit une instruction de plus que la double émission du Pentium 1. Le pipeline passe de 5 étage sur le Pentium à 14 - 12 étages, dont le détail est le suivant : * Prédiction de branchement, deux cycles ; * Chargement des instructions, trois cycles ; * Décodage de l'instruction, deux cycles ; * Renommage de registre, un cycle ; * Copie des opérandes dans le tampon de ré-ordonnancement (lié au renommage de registre dans le ROB) ; * Dispath dans ou depuis la station de réservation. * Exécution de l'instruction ; * Écriture du résultat dans le ROB ; * Écriture dans le banc de registre physique. Les instructions sont chargées par blocs de 16 octets, avec un système de fusion de blocs pour gérer les instructions à cheval sur deux blocs. Lors d'un branchement, deux blocs doivent être chargés si l'instruction de destination n'est pas alignée sur 16 octets et cela cause un délai de un cycle d'horloge. Le décodage des instructions x86 était géré par plusieurs décodeurs. Il y avait trois décodeurs : deux décodeurs simples, et un décodeur complexe. Les décodeurs simples décodaient les instructions les plus fréquentes, mais aussi les plus simples, qui étaient décodées en une seule micro-opération. Les instructions CISC complexes étaient gérées uniquement par le décodeur complexe, basé sur un microcode, qui pouvait fournir jusqu'à 4 micro-opérations par cycle. Le tout est résumé avec la règle 4-1-1. La toute première instruction chargée depuis la file d'instruction va dans le premier décodeur simple. Si jamais le décodeur ne peut pas décoder l'instruction, l'instruction est redirigée dans un autre décodeur, avec un délai d'un cycle d'horloge. Les stations de réservations étaient regroupées dans une structure centralisée, en sortie de l'unité de renommage. Elles avaient 5 ports d'émission, qui étaient sous-utilisés en pratique. Niveau ALU, on trouve deux ALUs entières, une flottante, une unité pour les instructions SSE et autres, et trois unités pour les accès mémoire (regroupées en une seule unité dans le schéma ci-dessous). Les unités mémoire regroupent une unité de calcul d'adresse pour les lectures, une autre pour les écritures, et une unité pour la gestion des données à écrire. Les unités de calcul d'adresse sont des additionneurs à 4 opérandes, complétement différents des ALU entières. Les ALU entières sont deux unités asymétriques : une ALU simple, et une ALU complexe incorporant un multiplieur. Les deux peuvent exécuter des opérations d'addition, soustraction, comparaison, etc. [[File:P6 func diag.png|centre|vignette|upright=2|P6 func diag]] Les premiers Pentium 3 n'avaient pas de cache L2 dans le processeur, celui-ci était sur la carte mère. Mais il a été intégré dans le processeur sur la seconde version du Pentium 3, la version Coppermine. Le Pentium 3 a servi de base aux microarchitectures d'Intel qui ont suivi. Les changements à chaque nouvelle génération sont assez mineurs : la prédiction de branchement est améliorée, la taille des stations de réservation et du ROB augmente, idem avec les autres structures liées à l'exécution dans le désordre. Les processeurs Intel ont conservé une fenêtre d'instruction centralisée, alors qu'AMD utilise une autre méthode, comme nous allons le voir dans ce qui suit. Les seuls changements notables sont est le passage à un renommage dans le ROB à un renommage à banc de registre physique, ainsi que l'introduction du cache de micro-opération. Et ce sont des modifications qu'AMD a aussi faites, celle-ci étant clairement une bonne idée pour toutes les micro-architectures avec un budget en transistor suffisant. Il est intéressant de garder cela en tête, car une bonne partie des améliorations de chaque micro-architecture proviendra de là. ===La microarchitecture Core=== La '''microarchitecture Core''' fait suite au Pentium 4, mais reprend en fait beaucoup d’éléments du Pentium 2 et 3. Elle utilise la station de réservation unique avec renommage dans le ROB, provenant du Pentium 2/3. Elle supporte aussi les optimisations des opérations ''load-up'', avec notamment un support des macro-opérations mentionnées plus haut. Les améliorations sont assez diverses, mais aussi assez mineures. * Le processeur incorpore un cache L2, en plus des caches L1 déjà présents auparavant. * La prédiction de branchement a été améliorée avec notamment l'ajout d'une ''Fetch Input Queue''. * L'architecture Core passe à la quadruple émission, soit une instruction de plus que sur le Pentium 2 et 3. Pour cela, un quatrième décodeur est ajouté, il s'agit d'un décodeur simple qui ne fournit qu'une seule micro-opération en sortie. * Un ''stack engine'' et un ''Loop Stream Detector'' ont été ajoutés, ainsi que le support de la macro-fusion qui fusionne une instruction de test et le branchement qui suit en une seule micro-opération. * Les techniques de désambiguïsation mémoire sont implémentées sur cette micro-architecture. Il y a quelques modifications au niveau de l'unité de chargement. La file d'instruction a toujours ce système de fusion de blocs, sauf que les branchements ne causent plus de délai d'un cycle lors du chargement. La file d'instruction est suivie par un circuit de prédécodage qui détermine la taille des instructions et leurs frontières, avant de mémoriser le tout dans une file de 40 instructions. La station de réservation dispose de 6 ports d'émission, mais on devrait plutôt dire 5. Sur les 5, il y en a un pour les lectures, un pour les écritures. Les deux sont reliées à une ''Load/Store Queue'', appelée ''Memory Ordering Buffer''. Elle est elle-même reliée au cache de données par deux ports : un port de lecture et un port d'écriture. Les trois ports d'émission restants sont connectés aux unités de calcul. Les trois ports récents sont chacun reliés à une ALU entière. L'additionneur flottant est connecté au port 1, alors que le multiplieur/diviseur flottante est connecté au port 0. Le fait de mettre les deux sur des ports différents permet d'émettre une addition et une multiplication flottant simultanément. Le multiplieur entier est relié au second port d'émission, celui sur lequel se trouve l'additionneur flottant. Le résultat que le processeur peut émettre un mix d'opérations flottantes et entière assez varié. [[Image:Intel Core2 arch.svg|centre|vignette|upright=2|Intel Core microarchitecture]] ===Les microarchitectures Sandy Bridge and Ivy Bridge=== Les micro-architectures suivant la micro-architecture Core ont introduit quelques grandes modifications : le passage à un renommage à banc de registre physique, l'ajout d'un cache de micro-opérations (et d'un ''Loop Stream Detector''). L'ajout du cache de micro-opérations est un gros changement, particulièrement avec le jeu d’instruction x86. Le décodage des instructions est lent, couteux en énergie. Mais avec l'introduction du cache de micro-opération, la majorité des micro-opérations est non pas décodée, mais lue depuis le cache de micro-opérations. Les décodeurs décodent les instructions pas encore exécutées, mais les exécutions suivantes sont lues depuis le cache de micro-opérations. Et vu la grande présence de boucles, le cache de micro-opérations est l'alimentation principale du pipeline. Les décodeurs servent surtout à alimenter le cache de micro-opérations, parfois décoder quelques instructions isolées exécutées de-dehors de boucles, pas plus. Concrètement, ils servent pour 10 à 20% des micro-opérations exécutées. Intel a d'ailleurs reflété ce fait dans sa terminologie. Intel distingue deux voies de chargement : le ''legacy pipeline'' et le cache de micro-opérations. L'unité de chargement et les décodeurs sont regroupés dans la voie du ''legacy pipeline''. Le cache de micro-opérations est complété avec un ''Loop Stream Detector'', placé après le cache en question. Les décodeurs et le cache de micro-opérations alimentent une file de micro-opérations, située juste avant l'étage de renommage de registres. La file de micro-opérations sert en quelque sorte de tampon entre l'étage de "décodage" et celui de renommage. Le ''Loop Stream Detector'' utilise cette file de micro-opérations comme d'un cache lorsqu'une boucle est détectée. Les micro-opérations de la boucle sont lue depuis la file de micro-opérations, pour être envoyée au renommeur de registres. L'avantage est que le cache de micro-opérations et/ou les décodeurs sont mis en pause et clock-gatés lorsqu'une boucle s'exécute, ce qui réduit la consommation du processeur. Le ''Loop Stream Detector'' et le cache de micro-opération ont globalement le même effet : désactiver tout ce qui est avant, le ''Loop Stream Detector'' appliquant cette méthode au cache de micro-opération lui-même.. Voyons maintenant quelles sont les micro-architectures qui implémentent ces optimisations. Les microarchitectures '''Sandy Bridge''' and '''Ivy Bridge''' sont similaires à l'architecture Core, si ce n'est pour le passage à un renommage à banc de registre physique, et l'ajout d'un cache de micro-opérations. Le nombre de ports d'émission passe à 7, avec 4 pour les instructions arithmétiques (flottantes comme entière), 2 pour les lectures, et un pour les écritures (en fait deux, avec un pour le calcul d'adresse, l'autre pour la donnée à écrire). Pour le reste, rien ne change si ce n'est la prédiction de branchement Les architectures '''Haswell''' et '''Broadwell''' ont ajouté quelques unités de calcul, élargit la sortie du cache de micro-opérations. Un port d'émission pour opération entières a été ajouté, de même qu'un port pour les accès mémoire. Le processeur passe donc à 8 ports d'émission, ce qui permet d'émettre jusqu'à 8 micro-opérations, à condition que le cache de micro-opération suive. Pour le reste, le processeur est similaire aux architectures précédentes, si ce n'est que certaines structures grossissent. L'architecture '''Skylake''' réorganise les unités de calcul et les ports d'émission pour gagner en efficacité. Pour le reste, les améliorations sont mineures. A la rigueur, l'unité de renommage de registre ajoute des optimisations comme l'élimination des MOV, les idiomes liés aux opérations avec zéro, etc. ===Les microarchitectures récentes d'Intel=== Les architectures '''Ice Lake''' et '''Tiger Lake''' passent de quadruple émission à la pentuple émission. Par contre, le processeur utilise toujours 4 décodeurs. Mais les micro-opérations étant émises depuis le cache de micro-opérations, ce n'est pas un problème pour la pentuple émission. Le processeur peut parfaitement émettre 5 micro-opérations en même temps, si elles sont lues depuis le cache de micro-opérations. Là encore, on voit à quel point le cache de micro-opération découple ce qu'il y avant de ce qu'il y a après. La microarchitecture '''Golden Cove''' altère les décodeurs et l'unité de chargement. Sur toutes les générations précédentes, on reste sur une unité de chargement qui charge 16 octets à la fois et il y a toujours 4 décodeurs identiques aux générations précédentes. Golden Cove passe à 6 décodeurs simples, et double la taille du chargement qui passe à 32 octets. Une telle stagnation sur les unités de chargement et de décodage s'explique encore une fois par la présence du cache de micro-opération fait que ce n'est pas trop un problème. Tout ce qui précède le cache de micro-opérations n'a pas de raison d'évoluer, car ce cache est très puissant. Niveau unités de calcul, le CPU a pas moins de 5 ALU entières, deux ''barrel shifters'', un multiplieur et deux unités de branchements. Le tout est répartit sur 5 ports d'émission. Pour les unités mémoire, il y a trois unités LOAD pour les lectures et deux unités STORE pour les écritures. Il n'y a plus d'unité flottante proprement dite, mais une unité SIMD qui est capable de faire plusieurs calculs flottants, qu'on ne détaillera pas ici, car nous n'avons pas encore vu les techniques de SIMD. Notons qu'il y a trois fenêtres d'instruction séparées pour : les instructions entières/flottantes, les lectures, les écritures. Le processeur peut décoder 6 instructions par cycle, et en émettre 6 à destination des fenêtres d'instructions. Les fenêtres d'instructions peuvent émettre 5 instructions entières, trois lectures et deux écritures en même temps. Le ROB accepte de ''commit'' 8 µops par cycle. [[File:Golden Cove.png|centre|vignette|upright=3|Golden Cove]] Il s'agit donc de processeurs superscalaires très larges, ce qui est la norme de nos jours et le restera pendant longtemps. Et il faut noter que même les cœurs basse performance sont dans ce cas. Pour rappel,n les CPU Intel modernes regroupent deux types de cœurs : les cœurs P et les cœurs E. Les cœurs P sont des cœurs haute performance, basés sur les microarchitectures Cove, optimisées pour la performance. Les cœurs E, quant à eux, utilisent une microarchitecture différente, conçue pour économiser de l'énergie et avoir une consommation réduite, au prix de performances réduites. Pourtant, même les coeurs E utilisent une superscalarité large ! Par exemple, voici ce que donne la microarchitecture Gracemont. Elle peut décoder deux paquets de 3 instructions chaque. Elle envoie ensuite 5 µops à l'unité d'émission, qui sont distribuées dans deux pipelines : un pipeline flottant/SIM et un pipeline entier. Et le pipeline entier peut émettre 4 µops entières, deux lectures, deux écritures et deux branchements ! Pire que ça, le ''barrel shifter'' et le multiplieur/diviseur sont dupliqués en deux exemplaires ! [[File:Gracemont.png|centre|vignette|upright=3|Gracemont]] ==Un étude des microarchitectures superscalaires x86 d'AMD== Les architectures Intel ont évolué progressivement, sans grandes cassure. Il y a une continuité presque initerrompue entre l'architecture du Pentium 2 et les architectures modernes. Intel a fait des améliorations mineures à chaque nouvelle micro-architecture, si on omet le passage à un renommage à banc de registre physique et l'ajout du cache de micro-opération. A l'opposé, les architectures AMD ont eu de nombreuses cassures dans la continuité où AMD a revu sa copie de fond en comble. Étudier ces architectures demande de voir trois choses séparément : le ''front-end'' qui regroupe l'unité de chargement et les décodeurs, le ''back-end'' qui gère l'exécution dans le désordre et les unités de calcul, et le sous-système mémoire avec les caches et la ''Load Store Queue''. Leur étude sera plus ou moins séparée dans ce qui suit, pour chaque classe d'architecture. ===La première génération de CPU AMD : les architectures K5, K6, K7, K8 et K10=== La première génération de processeurs AMD est celle des architectures K5, K6, K7, K8 et K10. Il n'y a pas de K9, qui a été abandonné en cours de développement. Les processeurs K5 et K6 portent ce nom au niveau commercial. Par contre, les processeurs d'architecture K7 sont aussi connus sous le nom d''''AMD Athlon''', les AMD K8 sont connus sous le nom d''''AMD Athlon 64''', et les architecture K10 sont appelées les '''AMD Phenom'''. Comme le nom l'indique, l'architecture K8 a introduit le 64 bits chez les processeurs AMD. Elles ont une architecture assez similaire pour ce qui est du chargement et des caches. Toutes disposent d'au minimum un cache L1 d'instruction et d'un cache L1 de données. Le K5 n'avait que ces caches, mais un cache L2 a été ajouté avec le K7, puis un L3 avec le K10. L'AMD K5 avait une TLB unique, mais les processeurs suivants avaient une TLB pour le L1 d'instruction et une autre pour le L1 de données. Idem pour le cache L2, avec deux TLB : une pour les données, une pour les instructions. Les caches L1/L2 sont de type exclusifs, à savoir que les données dans le L1 ne sont pas recopiées dans le L2. Le cache L2 est précisément un cache de victime, qui mémorise les données/instructions, évincées des caches L1 lors du remplacement des lignes de cache. L'introduction du cache L2 a entrainé l'ajout de deux TLB de second niveau : une L2 TLB pour les données et une autre pour les instructions. Les architectures K8 et K10 ont ajouté un cache L3, avec un accès indirect à travers l'interface avec le bus. : L'AMD K7 originel, aussi appelée Athlon classique, n'avait pas de cache L2, mais celui-ci était placé sur la carte mère et fonctionnait à une fréquence moitié moindre de celle du CPU. L'Athlon Thunderbird, puis l'Athlon XP, ont intégré le cache L2 dans le processeur. {|class="wikitable" |- ! Architecture AMD ! colspan="5" | Caches |- | rowspan="2" | K5 | L1 instruction || L1 données || colspan="3" | |- | colspan="2" | TLB unique || colspan="3" | |- | colspan="4" | |- | rowspan="2" | K6 | L1 instruction || L1 données || colspan="3" | L2 unifié |- | TLB L1 instruction || TLB L1 données || colspan="3" | |- | colspan="6" | |- | rowspan="2" | K7, K8 | L1 instruction || L1 données || colspan="2" | L2 unifié || |- | TLB L1 instruction || TLB L1 données || TLB L2 instruction || TLB L2 données || |- | colspan="6" | |- | rowspan="2" | K10 | L1 instruction || L1 données || colspan="2" | L2 unifié || L3 |- | TLB L1 instruction || TLB L1 données || TLB L2 instruction || TLB L2 données || |} Fait important, les architectures K5 à K10 utilisent la technique du '''prédécodage''', où les instructions sont partiellement décodées avant d'entrer dans le cache d'instruction. Le prédécodage facilite grandement le travail des décodeurs d'instruction proprement dit. Par contre, le prédécodage prend de la place dans le cache L1 d'instruction, une partie de sa capacité est utilisé pour mémoriser les informations prédécodées. C'est donc un compromis entre taille du cache et taille/rapidité des décodeurs d'instruction. Sur les architectures K5 et K6, le prédécodage précise, pour chaque octet, si c'est le début ou la fin d'une instruction, si c'est un octet d'opcode, en combien de micro-opérations sera décodée l'instruction, etc. A partir de l'AMD K7, le prédécodage reconnait les branchements inconditionnels. Lorsqu'un branchement inconditionnel est pré-décodé, le pré-décodage tient compte du branchement et continue le pré-décodage des instructions à partir de la destination du branchement. Le système de prédécodage est abandonnée à partir de l'architecture Bulldozer, qui suit l'architecture K10. La prédiction de branchement de ces CPU tire partie de ce système de pré-décodage, à savoir que les prédictions de branchement sont partiellement mémorisées dans les lignes de cache du L1 d'instruction. Par exemple, l'AMD K5 se passe de ''Branch Target Buffer'' grâce à cela. Si une ligne de cache contient un branchement, elle mémorise l'adresse de destination de ce branchement, en plus des bits de pré-décodage. Si il y a plusieurs branchements dans une ligne de cache, c'est l'adresse de destination du premier branchement pris dans cette ligne de cache qui est mémoirsée. Un défaut de cette approche est que si le branchement n'est pas dans le L1 d'instruction, aucune prédiction de branchement ne peut être faite et le préchargement ne peut pas fonctionner. C'est une limitation que n'ont pas les BTB découplées du cache L1 : elles peuvent prédire un branchement qui a été évincé dans le L2 ou le L3, tant que l'entrée associée est dans le BTB. Les prédictions peuvent même servir à précharger les instructions utiles. [[File:Comparaison du chargement de l'AMD K5 et K6.png|centre|vignette|upright=2|Comparaison du chargement de l'AMD K5 et K6]] Au niveau du décodage, on trouve de nombreuses différences entre les premières architectures AMD. L'AMD K5 contient 4 décodeurs hybrides, afin de décoder 4 instructions par cycles. Le K5 a quatre décodeurs simples couplés à 4 décodeurs complexes avec chacun un accès au micro-code. Une instruction peut donc passer par a donc deux voies de décodage : un décodage rapide et simple pour les instructions simples, un décodage lent et passant par le microcode pour les instructions complexes. Pour décoder 4 instructions, les deux voies sont dupliquées en 4 exemplaires, ce qui a un cout en circuits non-négligeable. L'AMD K6 utilise moins de décodeurs et ne peut que décoder deux instructions à la fois maximum. Par contre, il fournit en sortie 4 micro-opérations. Il intègre pour cela deux décodeurs simples, un décodeur complexe et un décodeur micro-codé. Un décodeur simple transforme une instruction simple en une ou deux micro-opérations. Il est possible d'utiliser les deux décodeurs simples en même temps, afin de fournir 4 micro-opérations en sortie du décodeur. Les deux autres décodent une instruction complexe en 1 à 4 micro-opérations. Si jamais la ou les deux instructions sont décodées en 1, 2 ou 3 micro-opérations, les micro-opérations manquantes pour atteindre 4 sont remplies par des NOPs. Pour le K7 et au-delà, le processeur dispose de décodeurs séparées pour les instructions micro-codées de celles qui ne le sont pas. Le processeur peut décoder jusqu’à 3 instructions par cycle. Le décodage d'une instruction microcodée ne peut pas se faire en parallèle du décodage non-microcodé. C'est soit le décodeur microcodé qui est utilisé, soit les décodeurs câblés, pas les deux en même temps. Le décodage d'une instruction prend 4 cycles. Les instructions non-microcodées sont décodées en une seule micro-opération, à un détail près : le CPU optimise la prise en charge des instructions ''load-up''. La différence entre le K6 et le K7 s'explique par des optimisations des instructions ''load-up''. Sur le K6, les instructions ''load-up'' sont décodées en deux micro-opération : la lecture en RAM, l'opération proprement dite. Mais sur le K7, une instruction ''load-up'' est décodée en une seule micro-opération. En conséquence, les décodeurs simples sont fortement simplifiés et le décodeur complexe disparait au profit d'un microcode unique. [[File:Décodage sur le K5 et le K5.png|centre|vignette|upright=3|Décodage sur le K5 et le K5]] ====Les microarchitectures K5 et K6 d'AMD==== Les deux premières architectures étaient les architectures K5 et K6, l'architecture K6 ayant été déclinée en quatre versions, nommées K6-1, K6-2, et K-3, avec une version K6-3 bis. Elles sont regroupées ensemble car elles ont beaucoup de points communs. Par exemple, tout ce qui a trait au chargement et au cache était similaire, de même que les unités de calcul. Les deux architectures avaient n'avaient pas de cache L2 et devaient se contenter d'un cache L1 d'instruction et d'un cache L1 de données. L'AMD K5 incorpore une TLB unique, alors que le K6 utilise des TLB séparées pour le cache d'instruction et le cache de données. Une différence entre l'architecture K5 et K6 est que la première utilise des caches normaux, alors que la seconde utilise des ''sector caches''. Les deux architectures disposaient des unités de calcul suivantes : deux ALU entières, une FPU, deux unités LOAD/STORE pour les accès mémoire, une unité de branchement et une ou plusieurs unités SIMD. Une organisation classique, donc. Pour les unités entières, il y avait deux ALU simples, un ''barrel shifter'' et un diviseur. Il n'y a pas d'erreur, le processeur incorpore un circuit diviseur, mais pas de circuit multiplieur. La raison est que la multiplication est réalisée par la FPU ! En effet, le multiplieur flottant de la FPU intègre un multiplieur entier pour multiplier les mantisses, qui est utilisé pour les multiplications entières. La même technique a été utilisée sur l'Atom, comme vu plus haut. Le tout était alimenté par deux ports d'émission, appelés ports X et Y. Sur l'architecture K5, le ''barrel shifter'' et le diviseur sont des ports différents. {|class="wikitable" |+ AMD K5 |- ! Port X ! Port Y |- | ALU simple | ALU simple |- | ''Barrel Shifter'' | Diviseur |} Sur l'architecture K6, le ''barrel shifter'' et le diviseur sont sur le même port. {|class="wikitable" |+ AMD K6 |- ! Port X ! Port Y |- | ALU simple | ALU simple |- | | ''Barrel Shifter'' |- | | Diviseur |} Niveau unités mémoire, le K5 avait deux unités LOAD/STORE, chacune capable de faire lecture et écriture. Par contre, la ''store queue'' n'a qu'un seul port d'entrée, ce qui fait que le processeur peut seulement accepter une écriture par cycle. Le processeur peut donc émettre soit deux lectures simultanées, soit une lecture accompagnée d'une écriture. Impossible d'émettre deux écritures simultanées, ce qui est de toute façon très rare. L'architecture K6 utilise quant à elle une unité LOAD pour les lectures et une unité STORE pour les écritures. Ce qui permet de faire une lecture et une écriture par cycle, pas autre chose. Niveau unités SIMD, l'architecture K7 n'avait qu'une seule unité SIMD, placée sur le port d'émission X. L'architecture K8 ajouta une seconde unité SIMD, sur l'autre port d'émission entier. De plus, trois ALU SIMD ont été ajoutées : un décaleur MMX, une unité 3DNow!, une unité mixte MMX/3DNow. Elles sont reliées aux deux ports d'émission entier X et Y ! Elles ne sont pas représentées ci-dessous, par souci de simplicité. [[File:Unité de calcul des processeurs AMD K5 et K6.png|centre|vignette|upright=2|Unité de calcul des processeurs AMD K5 et K6. les unités sur la même colonnes sont reliées au même port d'émission.]] Si les unités de calcul et le chargement sont globalement les mêmes, les deux architectures se différencient sur l'exécution dans le désordre. L'AMD K5 utilise du renommage de registre dans le ROB avec des stations de réservation. Par contre, l'AMD K6 utilise une fenêtre d'instruction centralisée. De plus, son renommage de registre se fait avec un banc de registre physique. L'architecture AMD K5 utilisait de deux stations de réservation par unité de calcul, sauf pour les deux unités mémoire partageaient une station de réservation unique (deux fois plus grande). Les stations de réservation sont cependant mal nommées, vu que ce sont en réalité des mémoire FIFO. Une micro-opération n'est émise que si elle est la plus ancienne dans la FIFO/station de réservation. Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique. Le tampon de ré-ordonnancement faisait seulement 16 instructions. [[File:AMD K5.jpg|centre|vignette|upright=3|AMDK5 Diagramme.]] L'architecture K6 remplace les stations de réservations par une fenêtre d'instruction centralisée. Les 4 micro-opérations renommées sont écrites dans la fenêtre d'instruction par groupe de 4, NOP de ''padding'' inclus. La fenêtre d'instruction centralisé contient 24 micro-opérations, groupées en 6 groupes de 4 micro-opérations, avec potentiellement des NOP dedans suivant le résultat du décodage. L'avantage est que l'implémentation de la fenêtre d'instruction est simple. La fenêtre d'instruction centralisée permettait d'émettre 6 micro-opérations en même temps (une par unité de calcul/mémoire). Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique. Le processeur utilisait un renommage avec un banc de registre physique. Le banc de registre physique pour les entiers contenait 48 registres, dont 24 étaient des registres architecturaux et 24 étaient des registres renommés. Sur les 24 registres architecturaux, 16 avaient une fonction de ''scratchpad'' que les ''datasheets'' d'AMD ne détaillent pas, les 8 restants étaient les registres généraux EAX, EBX, etc. [[File:AMD K6 Little foot & Modl 6.png|centre|vignette|upright=3|AMD K6 original.]] ====Les microarchitectures K7, K8 et K10 d'AMD==== Les microarchitectures suivantes sont les architectures K7, K8 et K10. Les architectures K7, K8 et K10 sont assez similaires. La différence principale entre le K7 et le K8 est le support du 64 bits. Les apports du K10 sont la présence d'un cache L3, d'une unité de calcul supplémentaire et d'améliorations de la prédiction de branchement. La taille de certains caches a été augmentée, de même que la largeur de certaines interconnexions/bus. L'architecture K7 des processeurs Athlon utilisait le renommage de registre, mais seulement pour les registres flottants, pas pour les registres entiers. Le ranommeg des registres flottants étaient réalisé via un banc de registres physique, ne contenant que des registres flottants. Les architectures K8 et K10 utilisent le renommage de registres pour tous les registres, entiers comme flottants. Par contre, le renommage de registre n'est pas réalisé de la même manière pour les registres entiers et flottants. Les registres entiers sont renommés dans le tampon de ré-ordonnancement, comme c'était le cas sur les architectures Intel avant le Pentium 4. Par contre, les registres flottants sont renommés grâce à un banc de registre physique. Le K8 est donc un processeur au renommage hybride, qui utilise les deux solutions de renommage principales. A partir du K7, le CPU optimise la prise en charge des instructions ''load-up''. Les instructions ''load-op'' sont appelées des macro-opérations dans la terminologie d'AMD, et aussi d'Intel. L'idée est que les instructions ''load-up'' sont décodées en micro-opérations intermédiaires. Elles sont propagées dans le pipeline comme étant une seule micro-opération, jusqu'à l'étage d'émission. Lors de l'émission, les instructions ''load-up'' sont scindées en deux micro-opérations : la lecture de l'opérande, puis l'opération proprement dite. Faire ainsi économise des ressources et optimise le remplissage du tampon de ré-ordonnancement, des fenêtres d'instructions, des stations de réservation, etc. Le tampon de réordonnancement est combiné avec divers circuits en charge de l'exécution dans le désordre, dans ce qui s'appelle l'''instruction control unit''. Il contient de 72 à, 84 instructions, qui sont regroupées en groupes de 3. Là encore, comme pour le K5 et le K6, le tampon de réordonnancement tient compte de la sortie des décodeurs. Les décodeurs fournissent toujours trois micro-opérations par cycle, quitte à remplir les vides par des NOP. Le tampon de réordonnancement reçoit les micro-opérations, NOP inclus, par groupes de 3, et est structuré autour de ces triplets de micro-opération, y compris en interne. Pour ce qui est de l'unité mémoire, elle est précédée par une file de µops mémoire, qui émet les accès mémoire dans l'ordre du programme. Elle est souvent qualifiée de ''Load-Store Queue'', mais ce n'est pas la terminologie que nous utilisons dans ce cours. La file de micro-opération lire/écrire 64 bits par cycle depuis le cache L1, ce qui fait un seul accès au cache par cycle. La file de µops mémoire est appelée la ''Pre-Cache Queue''. Si au vu de son nom, vous avez deviné qu'il y avait une ''Post-Cache Queue''. Elle mémorise les lectures/écritures émises, mais qui ont levé un défaut de cache L1. Elle ne fait pas partie de la file de µops mémoire proprement dite. Les architectures K7, K8 et K10 ont des unités de calcul très similaires. Concrètement, il y a trois ALU entières, trois unités de calcul d'adresse, et une FPU. Le processeur incorpore, aussi un multiplieur entier, relié sur le port d'émission de la première ALU. La FPU regroupe un additionneur flottant, un multiplieur flottant, et une troisième unité LOAD/STORE pour les lectures/écritures pour les nombres flottants. L'architecture K8 ajoute une unité de manipulation de bit, la K10 un diviseur entier. [[File:Unité de calcul des processeurs AMD K7, K8 et K10.png|centre|vignette|upright=2|Unité de calcul des processeurs AMD K7, K8 et K10]] La manière d'alimenter les ALU en micro-opérations varie un petit peu entre les architectures K7, K8 et K10. Il y a cependant quelques constantes entre les trois. La première est qu'il y a une fenêtre d'instruction séparée pour les flottants, de 36 à 42 entrées, avec renommage de registre. La fenêtre d'instruction flottante a trois ports d'émission : un pour l'additionneur flottant, un autre pour le multiplieur, et un troisième pour la troisième unité flottante qui s'occupe du reste. La seconde est que chaque ALU entière est couplée avec une unité de calcul d'adresse. Par contre, la méthode de couplage varie d'un processeur à l'autre. : Les stations de réservation sont nommées des ''schedulers'' dans les schémas qui suivent. La microarchitecture K7 avait deux fenêtres d'instruction : une pour les opérations flottantes, une autre pour les instructions entières et les accès mémoire. La fenêtre d'instruction entière était reliée à 3 ALU entières et à 3 AGU. Elle pouvait émettre trois micro-opérations en même temps : trois micro-opérations entières, trois micro-opérations mémoire. Les AGU étaient reliées à la file de µops mémoire mentionnée plus haut, ce qui permet d'émettre trois µops mémoire par cycle. Par contre, la file de µops mémoire ne pouvait exécuter qu'une lecture de 64 bits ou une écriture de 64 bits. En clair, trois micro-opérations mémoire peuvent être émises par cycle, cela entraine trois calculs d'adresse simultanés, mais les trois lectures/écritures sont mises en attente dans la file de µops mémoire. Elles s'exécutent alors l'une après l'autre. La fenêtre d'instruction entière contenait 5 à 6 groupes de 3 macro-opérations. Vous noterez que j'ai parlé de macro-opérations et pas de micro-opérations, car les instructions ''load-up'' sont considérées comme une seule "micro-opération" dans la fenêtre d'instruction entière. Et cela se marie bien avec une fenêtre d'instruction unique partagée entre pipeline entier et pipeline mémoire. Une macro-opération était scindée en deux micro-opérations : une micro-opération mémoire et une micro-opération entière. Il est donc avantageux de regrouper unités mémoire et unités entières à la même fenêtre d'instruction pour ce faire. [[File:AMD K7.png|centre|vignette|upright=3|AMD K7]] Sur les architectures K8 et K10, la station de réservation unique de 15 micro-opérations est remplacée par trois stations de réservations, de 8 micro-opérations chacune pour le K8, de 10 pour le K10. Chaque station de réservation entière alimente une unité de calcul entière et une unité de calcul d'adresse. l'unité de calcul d'adresse est reliée à la file de µops mémoire, qui n’exécute toujours qu'un seul accès mémoire par cycle. Le multiplieur est relié à la première station de réservation, sur le même port d'émission que l'ALU. [[File:AMD Husky microarchitecture.png|centre|vignette|upright=3|AMD Husky microarchitecture]] La microarchitecture K10 a été déclinée en plusieurs versions, nommées Grayhound, Grayhound+ et Husky, Husky étant une architecture gravée en 32 nm dédiée aux processeurs A-3000. L'architecture Grayhound a plus de cache et un ROB plus grand, la Husky est quand à elle un peu plus différente. Elle n'a pas de cache L3, contrairement aux autres architectures K10, ce qui simplifie fortement son sous-système mémoire. Par contre, les fenêtres d'instructions/stations de réservation et le ROB sont plus grands, pareil pour les files dans l'unité mémoire. Une ALU pour les divisions entières a aussi été ajoutée. Pour résumer, les architectures K7, K8 et K10 séparent les pipelines entiers et flottants : trois pipelines entiers avec chacun son unité de calcul, et un pipeline flottant avec plusieurs unités de calcul. Les raisons à cela sont assez diverses. Disons que dupliquer des ALU entières simples prend peu de transistors, là où les gros circuits comme le multiplieur ou la FPU ne sont pas dupliqués. Et cela a un autre avantage : le renommage, ''dispatch'' et l'émission sont plus simples. Les pipelines entiers ont une exécution dans le désordre peu complexe, grâce au grand nombre d'unités de calcul, ce qui fait que le pipeline entier est de seulement 15 cycles au total (chargement et décodage inclus). A l'opposé, la FPU est alimentée par une exécution dans le désordre très complexe, avec banc de registre physique et beaucoup de ressources, mais au prix d'un pipeline flottant plus long de 3 cycles, soit 18 cycles au total. ===Les microarchitectures ZEN d'AMD=== Viennent ensuite les '''microarchitectures Bulldozer''', avec trois révisions ultérieures nommées Piledriver, Steamroller et Excavator. Mais du fait de l'utilisation de techniques de multithreading matériel que nous n'avons pas encore abordé, nous ne pouvons pas en parler ici. Les microarchitectures suivantes sont les '''architectures ZEN 1/2/3/4/5'''. Elles se ressemblent beaucoup, chacune accumulant les améliorations des précédentes. Mais le cœur de l'architecture reste plus ou moins le même. En passant à la suivante, le nombre de registre virtuel augmente, le ''branch target buffer'' augmente en taille, le ROB et les files d'attente grossissent, les caches de micro-opération aussi, les caches grossissent, etc. Une optimisation intéressante est l'ajout d'un cache de micro-opération, qui améliore grandement les performances du ''front-end'', notamment pour les boucles. La microarchitecture Zen 1 est illustrée ci-dessous. Comme on le voit, les registres flottants ont une unité de renommage séparée de celle pour les entiers, mais les deux utilisent du renommage à banc de registre physique. Il y a par contre une différence au niveau des fenêtres d'instruction, notées ''scheduler'' dans le schéma. Pour ce qui est des unités de calcul flottantes, il y a une fenêtre unifiée qui alimente quatre ALU, grâce à 4 ports d'émission. Mais pour les ALU entières, il y a une fenêtre d'instruction par ALU, avec un seul port d'émission connecté à une seule ALU. La raison de ce choix est que les opérations flottantes ont un nombre de cycle plus élevé, sans compter que les codes flottants mélangent bien additions et multiplication. Une fois décodées, les instructions sont placées dans une première file de micro-opérations om elles attendent, puis sont dispatchées soit dans le pipeline entier, soit dans le pipeline flottant. les micro-opérations entières sont insérées dans une fenêtre d'instruction directement, alors que les micro-opérations flottantes doivent patienter dans une seconde file de micro-opérations. La raison est que les micro-opérations flottantes ayant une grande latence, trop d'instructions flottantes consécutives pourraient bloquer le pipeline flottant, sa fenêtre d'instruction étant pleine. Le pipeline flottant étant bloqué, la première file de micro-opérations serait bloquée et on ne pourrait plus émettre de micro-opérations entières. Pour éviter cela, une solution serait d'agrandir la file de micro-opérations, mais cela la rendrait plus lente et se ferait donc au détriment de la fréquence d'horloge. Alors une solution a été d'ajouter une seconde file de micro-opérations, au lieu d'agrandir la première. [[File:Zen microarchitecture.svg|centre|vignette|upright=3|Microarchitecture Zen 1 d'AMD.]] Le passage à la microarchitecture n'a pas causé de grands changements. Le Zen 2 a ajouté une unité de calcul d'adresse, ce qui fait qu'on passe à 4 ALU, 3 AGU et 4 FPU. La fenêtre d'instruction flottante reste la même. Par contre, les fenêtres d'instruction entières changent un peu. Ou plutot devrais-je dire les fenêtres d'instruction mémoire. En effet, le Zen 2 fusionne les fenêtres d'instructions liées aux AGU en une seule fenêtre d'instruction deux fois plus grosse. Le Zen 5 a ajouté deux autres ALU entières et une unité de calcul d'adresse (6 ALU / 4 AGU) ==Les processeurs Atom d'Intel, de microarchitecture Bonnell== L'architecture de l'Atom première génération est assez simple. Son pipeline faisait 16 étages, ce qui est beaucoup. C'est un processeur 32 bits, ce qui aura son importance dans ce qui suit. Il était conçu pour être un processeur basse consommation, donc peu puissant. En conséquence, il n'a pas d'exécution dans le désordre, même s'il est superscalaire. C'était la norme à l'époque pour les processeurs basse consommation, que de faire sans exécution dans le désordre. De nos jours, les choses ont bien changée, même les processeurs basse consommation ont exécution dans le désordre, superscalarité et renommage de registres. ===Le ''front-end'' de l'Atom=== Le cache d'instruction permet de lire 8 octets par cycle, qui sont placés dans une file d'instruction, elle-même suivie par deux décodeurs. Le fait que les décodeurs lisent les instructions depuis une file d'instruction fait que les deux instructions décodées ne sont pas forcément consécutives en mémoire RAM. Par exemple, l'Atom peut décoder un branchement prédit comme pris, suivi par l'instruction de destination du branchement. Les deux instructions ont été chargées dans la file d'instruction et sont consécutifs dedans, alors qu'elles ne sont pas consécutives en mémoire RAM. Sur l'Atom, la majorité des instructions x86 sont décodées en une seule micro-opération, y compris les instructions ''load-up''. Le microcode n'est utilisé que pour une extrême minorité d'instructions et est à part des deux décodeurs précédents. L'avantage est que cela permet d'utiliser au mieux la file de micro-opération, qui est de petite taille. Mais surtout, cela permet de grandement réduire la consommation du processeur, au détriment de ses performances. Pour avoir un décodage rapide, malgré des instructions complexes, le processeur recourt à la technique du pré-décodage, qui prédécode les instructions lors de leur chargement dans le cache d'instruction. Le prédécodage lui-même prend deux cycles, là où une lecture dans le L1 d'instruction en prend 3. les défauts de cache d'instruction sont donc plus longs de deux cycles. Mais l'avantage du prédécodage est que la consommation d'énergie est diminuée. Prenez une instruction exécutée plusieurs fois, dans une boucle. Au lieu de décoder intégralement une instruction à chaque fois qu'on l'exécute, on la prédécode une fois, seul le reste du décodage est fait à chaque exécution. D'où un gain d'énergie assez intéressant. Les caches de micro-opération, qui sont capables d'exécuter une optimisation similaire, n'existaient pas encore à cette époque. ===Le chemin de données de l'Atom=== Les deux décodeurs alimentent une file de micro-opérations de petite taille : 32 µops maximum, 16 par ''thread'' si le ''multithreading'' matériel est activé. La file de micro-opérations a deux ports d'émission, ce qui permet d'émettre au maximum 2 µops par cycle. Les conditions pour cela sont cependant drastiques. Les deux instructions ne doivent pas avoir de dépendances de registres, à quelques exceptions près liées au registre d'état. Le multithreading matériel doit aussi être désactivé. Les deux instructions doivent aller chacun dans un port différent, et cela tient en compte du fait que les deux ports sont reliés à des unités de calcul fort différentes. Le tout est illustré ci-dessous. Les deux ports ont chacun une ALU simple dédiée, capable de faire des additions/soustractions, des opérations bit à bit et des copies entre registres. Mais ils ont aussi des opérations qui leur sont spécifiques. La séparation entre les deux pipelines est assez complexe. Il ne s'agit pas du cas simple avec un pipeline entier et un pipeline flottant séparés. En réalité, il y a deux pipelines, chacun capables de faire des opérations entières et flottantes, mais pas les mêmes opérations. Le premier port permet d’exécuter des opérations entières simples, une addition flottante, des comparaisons/branchements, ou une instruction de calcul d'adresse LEA. Le second port/pipeline est, quant à lui, conçu pour exécuter les instruction ''load-up'' nativement, en une seule micro-opération. Il contient toute la machinerie pour faire les accès mémoire, notamment des unités de calcul d'adresse et un cache L1 de données. A la suite du cache, se trouvent une ALU entière simple, un ''barrel shifter'', et un circuit multiplieur/diviseur. Le circuit multiplieur/diviseur est utilisé à la fois pour les opérations flottantes et entières. [[File:Intel Atom Microarchitecture.png|centre|vignette|upright=2.5|Intel Atom Microarchitecture]] Cette organisation difficile à comprendre est en réalité très efficace, très économe en circuit, tout en gardant une performance intéressante. Les instructions simples, ADD/SUB/bitwise sont supportées dans les deux pipelines. Il faut dire que ce sont des opérations courantes qu'il vaut mieux optimiser au mieux. Le processeur peut donc émettre deux opérations simples et fréquentes en même temps, ce qui augmente les performances. Les opérations plus complexes, à savoir les multiplications/divisions/décalages/rotations/manipulations de bit sont supportées dans un seul pipeline. La raison est qu'il est rare que de telles opérations soient consécutives, et qu'il n'est donc pas utile d'optimiser pour cette situation. Si les deux pipelines devaient supporter ces opérations, cela demanderait de dupliquer les circuits multiplieurs/diviseur, ce qui aurait un cout en circuit important pour un gain en performance assez faible. ===Le système d'exceptions flottantes de l'Atom=== Le processeur étant sans exécution dans le désordre, ses instructions doivent écrire dans les registres dans l'ordre du programme. En conséquence, certaines instructions doivent être retardées, leur émission doit attendre que les conditions soient adéquates. Et cela pose problème avec les opérations flottantes, vu qu'elles prennent pas mal de cycles pour s'exécuter. Imaginez qu'une instruction flottante de 10 cycles soit suivie par une instruction entière. En théorie, on doit retarder l'émission de l'instruction entière de 9 cycles pour éviter tout problèmes. Le cout en performance est donc assez important. En théorie, les instructions entières et flottantes écrivant dans des registres séparés, ce qui fait que l'on pourrait exécuter instructions entières et flottantes dans le désordre. Sauf pour les instructions de copie entre registres entier et flottants, mais laissons-les de côté. Le problème est qu'une instruction flottante peut parfois lever une exception, par exemple en cas de division par zéro, ou pour certains calculs précis. Si une exception est levée, alors l'instruction flottante est annulée, de même que toutes les instructions qui suivent, y compris les opérations entières. Ce n'est pas un problème si le processeur gère nativement les exceptions précises, par exemple avec un tampon de ré-ordonnancement. Mais l'Atom étant un processeur sans exécution dans le désordre, les instructions entières devraient être mises en attente tant qu'une instruction flottante est en cours d'exécution. Heureusement, l'Atom d'Intel a trouvé une parade. La technique, appelée ''Safe Instruction Recognition'' par Intel, est décrite dans le brevet US00525721.6A. L'idée est de tester les opérandes flottantes, pour détecter les combinaisons d'opérandes à problème, dont l'addition/multiplication peut lever une exception. Si des opérandes à problème sont détectées, on stoppe l'émission de nouvelles instructions en parallèle de l'instruction flottante et l'unité d'émission émet des bulles de pipeline tant que l'instruction flottante est en cours. Sinon, l'émission multiple fonctionne. La technique permet ainsi de ne pas écrire dans les registres entiers/flottants dans l'ordre du programme : une instruction entière peut être autorisée à s'exécuter même si elle écrit dans un registre entier avant qu'une instruction flottante délivre son résultat. ==La microarchitecture Netburst du Pentium 4== Dans cette section, nous allons voir l'architecture du processeur Pentium 4, qu'on a volontairement laissée de côté précédemment. Pourquoi un tel saut dans le temps ? Parce que le Pentium est complément à part des autres architectures Intel. Le Pentium 4 a représenté une rupture en termes de microarchitecture, qui a été un échec tellement retentissant que les processeurs suivants sont repartis sur la base du Pentium 3. Il introduisait de nombreuses nouveautés architecturales qui étaient très innovantes. Par exemple, il introduisait le renommage avec un banc de registre physique, qui a été utilisé sur tous les processeurs Intel suivants. Mais la plupart de ces innovations étaient en réalité de fausses bonnes idées, ou du moins des idées difficiles à exploiter. Par exemple, le système de pipeline à ''replay'' n'a été utilisé que sur le Pentium 4 et aucun autre processeur ne l'a implémenté. ===Un focus sur la fréquence d'horloge=== La microarchitecture du Pentium 4 a été déclinée en plusieurs versions, dont les finesses de gravure n'étaient pas les mêmes. La microarchitecture Netburst, utilisée sur le Pentium 4, utilisait un pipeline à 20 étage, augmenté à 32 sur une révision ultérieure. Il a existé quatre révisions de l'architecture : Willamette (180 nm), Northwood (130 nm), Prescott (90 nm) et Cedar Mill (65 nm). Un point important est que le Pentium 4 était prévu pour fonctionner à haute fréquence. Ses 1,5 GHz étaient impressionnants pour l'époque, les autres processeurs tournant à une fréquence proche du GigaHertzs. Pour cela, la solution retenue par Intel a été un pipeline très long, avec beaucoup d'étages. Le Pentium 4 a été décliné en plusieurs versions assez proches, chacune avec sa propre finesse de gravure qui n'ont pas toute le même pipeline. Les micro-architectures ''Willamette'' et ''Northwood'' avaient un pipeline de 20 étages, alors que les autres processeurs de l'époque avaient entre 10 et 15 étages maximum. Les micro-architectures ''Prescott'' et ''Cedar Mill'' étaient une refonte qui a fait grimper le nombre d'étages à 31 ! Du jamais vu, il s'agit d'un record pour un processeur commercial. Un pipeline aussi long permet d'exécuter beaucoup d’instructions en même temps, chacune dans un étage, mais aussi d'atteindre de hautes fréquences facilement. Le problème est qu'un pipeline avec autant d'étages a beaucoup de problèmes. Un point important est que la prédiction de branchement est cruciale. Pour rappel, la pénalité en cas de mauvaise prédiction dépend du nombre d'étages avant que le branchement soit résolu. Et les branchements sont résolus soit en fin de décodage, soit dans l'unité de calcul. C'est à dire au milieu du pipeline, soit en fin de pipeline. La pénalité en cas de mauvaise prédiction de branchement était énorme sur le Pentium 4, elle atteignait facilement 30 cycles Pour compenser, le Pentium 4 avait une prédiction de branchement très performante, pour l'époque. J'insiste sur le pour l'époque. Il utilisait un prédicteur qu'on a déjà abordé dans le chapitre sur la prédiction de branchement, précisément un prédicteur adaptatif à deux niveaux avec un historique global de 16 bits. Il avait aussi un ''Branch Target Buffer'' de 4096 entrées. Mais surtout, il intégrait une sorte de précurseur du cache de micro-opération, appelé le cache de traces, qui est détaillé dans la section suivante. ===Le cache de trace du Pentium 4=== Les décodeurs du Pentium 4 ne font pas décoder les instructions, ils mémorisent le résultat dans un cache de micro-opération un peu particulier, appelé le '''cache de trace'''. Une ligne de cache peut mémoriser 6 micro-opérations, ce qui peu sembler peu mais a été repris sur les micro-architectures suivantes. Mais le cache de trace a une grande différence avec un cache de micro-opération normal. Un cache de micro-opération normal mémorise une instruction par ligne de cache. Une instruction est décodée en plusieurs micro-instructions, qui sont enregistrées dans une ligne de cache. Si l'instruction n'utilise par les 6 micro-opérations disponibles, le reste de la ligne de cache n'est pas utilisé. Mais le Pentium 4 optimise le tout de manière ce à ce que ne soit pas le cas. Sur le Pentium 4, la contrainte du "une instruction par ligne de cache" est abandonnée. Une ligne de cache mémorise 6 micro-opérations consécutives, qui peuvent appartenir à plusieurs instructions. Par exemple, si le décodeur décode 4 instructions consécutives en 6 micro-opérations au total, alors le tout prendra une seule ligne de cache sur le Pentium 4. Et les 4 instructions consécutives n'ont même pas à être consécutives en mémoire : il peut y avoir des branchements pris entre ces instructions ! {|class="wikitable" |+ Cache de trace |- ! Ligne de cache | ADD || SUB || ADD || MOV || MUL || ''shift'' |- ! Ligne de cache | colspan="3" | ADD ''load-up'' || MUL || colspan="2" | Branch if Equal |- ! Ligne de cache | XOR || colspan="4" | POP || SUB |- ! ... | colspan="6" | ... |} Pour expliquer cela plus concrètement, nous allons devoir introduire les concepts de trace et de bloc de base. Un '''bloc de base''' (''basic block'') est une suite d'instructions sans branchement, qui est séparé par deux branchements. Le début d'un bloc de base est la destination d'un branchement, un bloc de base se termine avec un branchement. Une '''trace''' est formée en concaténant plusieurs blocs de base. Pour donner un exemple, regardez le code illustré ci-contre. Il est composé d'un bloc de base A, suivi par un bloc de base B, qui peut faire appel soit au bloc C, soit un bloc D. Un tel code peut donner deux traces : ABC ou ABD. La trace exécutée dépend du résultat du branchement qui choisit entre C et D. Le cache de trace mémorise des traces de 6 micro-opérations consécutives. Les traces sont formées en sortie des décodeurs d'instruction, par de subtiles opérations mélangeant mémorisation, décalage et concaténation. Les circuits qui construisent les traces ne sont pas connus, mais ils doivent certainement être très compliqués. toujours est-il qu'un cache de trace peut mémoriser des traces différentes, même si leur début est le même. Par exemple, prenons deux traces, composées des blocs de base A, B, C et D. La première trace est la trace ABC, la seconde est la trace ABD. Les deux traces auront chacune une ligne de cache dédiée. Une trace est réutilisable quand le premier bloc de base est identique et que les prédictions de branchement restent identiques. Pour vérifier cela, le tag du cache de traces contient l'adresse du premier bloc de base, la position des branchements dans la trace et le résultat des prédictions utilisées pour construire la trace. Le résultat des prédictions de branchement de la trace est stocké sous la forme d'une suite de bits : si la trace contient n branchements, le n-ième bit vaut 1 si ce branchement a été pris, et 0 sinon. Même chose pour la position des branchements dans la trace : le bit numéro n indique si la n-ième instruction de la trace est un branchement : si c'est le cas, il vaut 1, et 0 sinon. Si la trace est réutilisée par la suite, elle est lue depuis le cache de traces. Pour savoir si une trace est réutilisable, l'unité de chargement envoie le ''program counter'' au cache de traces, l'unité de prédiction de branchement fournit le reste des informations. Si on a un succès de cache de traces, et la trace est envoyée directement au décodeur. Sinon, la trace est chargée depuis le cache d'instructions et assemblée. Il faut signaler que le cache de trace avait sa propre unité de prédiction de branchement séparée de l'unité de prédiction de branchement normale. [[File:TraceCache.png|centre|vignette|upright=2|Cache de traces.]] Le cache de traces réduisait la longueur du pipeline en cas de succès de cache de trace. Quand les instructions étaient lues depuis le cache de trace, les étages avant le cache de trace ne sont pas utilisés, tout se passe comme s'ils étaient retirés du pipeline. C'est la même chose avec le cache de micro-opération des processeurs modernes, mais l'idée n'existait pas encore à l'époque. Le cache de trace mémorise des traces décodées, ce qui fait qu'un succès de cache de trace contournait non seulement le cache d'instruction, mais aussi les décodeurs. Le temps d'accès au cache de trace pouvait être assez élevé, même s'il était comparable au temps d'accès du cache d'instruction. Le cache de traces a depuis été remplacé par une alternative bien plus intéressante, le cache de micro-opérations, plus flexible et plus performant. Comparé à un cache de trace, la contrainte "une instruction par ligne de cache" simplifie grandement l'implémentation d'un cache de micro-opération. Ne parlons pas de la détection des succès de cache, qui demande d'utiliser les prédictions de branchement. Mais le vrai problème avec le cache de trace est tout autre. Il arrive souvent qu'une micro-opération soit présente dans plusieurs lignes de cache en raison du processus de construction des traces, chose impossible avec un cache de micro-opération. Et c'est un problème, qui réduit la capacité effective du cache de trace. Alors certes, une ligne de cache est plus remplie que sur un cache de micro-opération, on est certain que les 6 micro-opération par ligne de cache sont remplies. Mais la redondance réduit grandement cet avantage. ===L'exécution dans le désordre et le chemin de données du P4=== Le renommage de registres se fait avec un banc de registres physiques avec une table d'alias. Le Pentium 4 avait une exécution dans le désordre très limitée, basée sur la présence de deux files de micro-opération : une pour les accès mémoire, une autre pour les autres instructions. La seconde file regroupait opérations entières et flottantes, elles n'étaient pas séparées. Avec ces deux files, les instructions mémoire étaient exécutées dans l'ordre du programme, les instructions arithmétiques s'exécutaient aussi dans l'ordre du programme, mais une instruction arithmétique pouvait passer avant une instruction mémoire et inversement. L'avantage est que cela permettait de faire des lectures en avance, c'était une forme limitée de lecture non-bloquantes. Il s'agit bel et bien de deux files d'instructions, pas de fenêtres d'instruction ni de stations de réservation. Le Pentium 4 est le seul processeur commercial qui a utilisé des files de micro-opération séparées, tous les autres utilisent des fenêtres d'instruction : centralisées pour Intel, décentralisées pour AMD (en général). Le Pentium 2 et 3, bien qu'antérieurs, utilisait une station de réservation unique. Cela peut sembler être un retour en arrière, mais les files d'instructions sont bien plus larges : de 42 micro-opérations pour le Pentium 3, on passe à 120 micro-opérations pour le Pentium 4. Et vu la longueur du pipeline, qui fait qu'il y a plus d'instructions en vol, c'était une nécessité. Mais cela n'aurait pas été possible en utilisant des stations de réservation, pour des raisons de consommation électrique et/ou de budget en transistors, ce qui fait que passer à une file de micro-opération été la solution retenue. Pour les accès mémoire, le Pentium 4 utilisait donc une file de µops mémoire unique, couplée à une file d'écriture (non-représentée sur les schémas qui suivent). La file d'écriture du Pentium 4 était de 24 écritures maximum et gérait le ''Store-to-load forwarding''. Le processeur pouvait émettre une lecture et une écriture à chaque cycle. Il y avait un port d'émission pour les lectures et un autre pour les écritures, tous deux ayant chacun leur propre unité de calcul d'adresse. Le processeur contenait 3 ALU entières, 2 unités de calcul d'adresse et une FPU. La FPU était complétée par une unité pour faire des copies entre registres flottants, des opérations MOV. Pour les AGU, il y en avait une dédiée aux lectures, une autre pour les écritures. Le tout était relié aux ports d'émissions comme suit : [[File:Ports d'émission du Pentium 4.png|centre|vignette|upright=2.5|Ports d'émission du Pentium 4]] Le processeur utilisait deux réseaux de contournement séparés : un pour les opérations flottantes, un pour les opérations entières. Le réseau de contournement pour les opérations entières est aussi relié aux unités de calcul d'adresse. Jusque là, rien de surprenant, le chemin de données du processeur est assez classique. [[File:Architettura Pentium 4.png|centre|vignette|upright=3|Microarchitecture du Pentium 4.]] ===Les unités de calcul entières du Pentium 4=== Sur le Pentium 4, les ALU entières étaient cadencées à une fréquence double de celle du processeur. Les ALU entières pouvaient exécuter deux micro-opérations par cycle, ce qui fait que les ports d'émissions reliés aux ALU devaient eux aussi fonctionner à double fréquence. Pour faire la différence entre les deux fréquences, nous parlerons de fréquence/cycle processeur et de fréquence/cycle de l'ALU. Précisons que seules les ALU entières étaient à double fréquence, pas le multiplieur, pas le ''barrel shifter''. Pour simplifier, nous allons parler d'additionneur plutôt que de l'ALU entière, ce qui sera plus proche de la réalité. Et l'implémentation de l'additionneur du Pentium 4 était très innovante. L'additionneur pouvait exécuter deux additions par cycle, même si les deux additions ont une dépendance. Mais n'allez pas croire que l'implémentation était intuitive, avec un additionneur 32 bit basique très rapide. Non seulement l'additionneur fonctionnait à double fréquence, mais il était aussi pipeliné, avec un système de contournement interne ! Les additionneurs étaient pipelinées, d'une manière très simple. Une addition 32 bits était découpée en trois étapes : deux additions de 16 bits, une dernière étape pour mettre à jour le registre d'état. Pour cela, chaque additionneur était composé de deux additionneur 16 bits chacune, placées l'une après l'autre, avec un registre de pipeline entre les deux. L'additionneur prenait deux cycles d'horloge pour faire son travail : le premier cycle calculait les 16 bits de poids faible, le second calculait les 16 bits de poids fort lors du second cycle. Le tout est appelé '''addition étagée''' (''staggered add'') dans la documentation Intel. Une addition se fait donc en deux étapes, sauf que c'est compensé par le fait que l'additionneur fonctionnait à une fréquence double de celle du processeur ! Le résultat de ce fonctionnement franchement bizarre, est que les 16 bits de poids faible étaient calculés en une moitié de cycle processeur, alors que l'opération complète prenait un cycle. Deux additions consécutives s'exécutaient donc en 1 cycle et demi, alors qu'on aurait cru au premier abord que cela prendrait seulement un cycle. Si on fait les calculs, on s'apercoit que le rythme de croisière est cependant proche de 2 additions par cycle, bien qu'inférieur. 3 additions consécutives se font en deux cycles, 5 additions en 3 cycles, 7 en 4 cycles, etc. Et le Pentium 4 ajoutait un système de contournement interne à l'ALU. En clair, si une addition utilise le résultat de l'addition précédente, les deux peuvent s'exécuter en un cycle d'horloge et demi. Les 16 bits de poids faible de la première addition sont disponibles après un cycle ALU, ce qui permet de démarrer le calcul de la seconde addition au cycle suivant. ===Le ''replay pipeline''=== Le processeur est un processeur triple émission : il peut charger et décoder 3 µops par cycle, le ROB peut terminer 3 µops par cycle, etc. Pourtant, les ports d'émission peuvent émettre 6 instructions par cycle : 4 ports, dont deux à double fréquence. Une telle différence s'explique par l'usage d'un ''replay pipeline'', dont nous avons déjà parlé dans ce cours. Pour rappel, le Pentium 4 suppose que les lectures font tous un succès de cache L1. Si une opération arithmétique utilise la donnée lue comme opérande, le processeur l'émet immédiatement, l'opérande sera disponible une fois l'opération en entrée de l'ALU entière. Mais s'il s'est trompé, le processeur ré-exécute l'instruction après un temps d'attente de quelques cycles, pour se caler sur la latence du cache L2. Et si il y a un défaut de cache L2, l’instruction attend encore. Cela demande de ré-exécuter des instructions émises à tord, ce qui fait que le processeur doit avoir la capacité d'exécution pour. Ce pourquoi le processeur peut émettre 6 µops dans les unités de calcul : 3 µops normales et 3 µops ré-exécutées. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Les processeurs superscalaires | prevText=Les processeurs superscalaires | next=Les processeurs VLIW et EPIC | nextText=Les processeurs VLIW et EPIC }} </noinclude> jl5ss0cxpd553x5nuii2yb1o56fhm9q 772757 772756 2026-09-21T21:27:25Z Mewtow 31375 /* Les microarchitectures récentes d'Intel */ 772757 wikitext text/x-wiki Dans ce chapitre, nous allons étudier des exemples de processeurs x86, ceux présents dans nos PC. Nous n'allons pas voir les anciens processeurs comme le 286, le 386 ou le 486. Nous allons commencer avec le Pentium 1, et les processeurs commerciaux qui ont suivis. Tous les processeurs que nous allons voir dans ce chapitre sont des processeurs superscalaires. De fait, ce n'est pas pour rien si ce chapitre se situe après le chapitre sur les processeurs superscalaires. Par contre, nous allons voir que certains n'ont pas d'exécution dans le désordre. La raison est que l'exécution dans le désordre est arrivé après la superscalarité. ==Généralités sur les CPU x86 superscalaires== Avant de voir chaque processeur indépendamment des autres, nous allons devoir aborder quelques généralités. Nous allons d'abord voir que le jeu d'instruction x86 pose quelques problèmes pour la superscalarité. Et ces problèmes posent des contraintes assez fortes, avec lesquelles les concepteurs de processeurs dovient faire avec. Nous poursuivrons ensuite par un historique des processeurs Intel et AMD, histoire de donner un peu de contexte aux processeurs que nous allons étudier. ===Le jeu d'instruction x86 pose des problèmes pour la superscalarité=== Une difficulté de l'architecture x86 est qu'il s'agit d'une architecture CISC, avec tous les défauts que ça implique. Un jeu d'instruction CISC a en effet de nombreuses propriétés qui collent mal avec l'émission multiple, avec la '''superscalarité'''. Il y en a plusieurs, certaines impactent le chargement des instructions, d'autres leur décodage, d'autres l'exécution, etc. Premièrement, les instructions sont de longueur variable, entre 1 et 15 octets, ce qui complique leur chargement et leur décodage. En pratique, les processeurs chargent un bloc de 32 à 64 octets, et découpent celui-ci en plusieurs instructions. La conséquence est que l'usage d'instructions trop longues peut poser problème. Imaginez qu'un processeur charge un bloc de 16 octets et que celui-ci ne contienne qu'une seule instruction : on ne profite pas de la superscalarité. Deuxièmement, une partie des instructions est microcodée, faute de mieux. Et cela pose de sérieux challenges pour l'implémentation des décodeurs. Dupliquer le microcode demanderait trop de transistors, ce qui fait que ce n'est pas fait. À la place, il n'y a qu'un seul microcode, ce qui fait que l'on ne peut pas décoder plusieurs instructions microcodées en même temps. Il est cependant possible de profiter de la superscalarité, en décodant une instruction microcodée en parallèle d'autres instructions non-microcodées. Et heureusement, ce cas est de loin le plus fréquent, il est rare que plusieurs instructions microcodées se suivent. Troisièmement, la présence d'instructions ''load-up'', qui lisent un opérande en mémoire, peut poser problème, mais est aussi source d'optimisations assez intéressantes. En théorie, une instruction ''load-op'' est décodée en deux micro-opération : une pour lire d'opérande en RAM, l'autre pour faire l'opération arithmétique. Sauf que les processeurs x86 modernes optimisent la gestion des instructions ''load-up''. Par exemple, les premiers processeurs Atom géraient des micro-opérations de type ''load-up'', directement dans le chemin de données ! D'autres processeurs utilisent la technique de la '''micro-fusion''' pour retarder le décodage réel des instructions ''load-up'' assez loin dans le pipeline. Avec eux, une instruction ''load-op'' est décodée en une seule "macro-opération", qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. L'avantage est qu'une macro-opération ne prend qu'une seule entrée dans le tampon de ré-ordonnancement, la fenêtre d'instruction, la file de micro-opération, et les autres structures similaires. ===Un petit historique des processeurs x86 superscalaires=== Nous allons voir les processeurs Intel à part des processeurs AMD. La raison à cela est que les architectures Intel et AMD ont progressivement évolué, chacune se basant sur la précédente et l'améliorant. Il n'y a pas eu de cassure entre microarchitectures AMD, qui sont chacune la suite de la précédente. Il est donc préférable de voir les architectures AMD dans l'ordre chronologique. Par contre, Intel a eu une gigantesque cassure, avec le processeur Pentium 4. Son architecture se démarquait fortement du Pentium 3, mais elle n'a pas convaincu et a été abandonnée avec les processeurs suivants. Ce qui fait nous verrons l'architecture du Pentium 4 à part. Le Pentium 1 et 2 utilisaient la même architecture, qu'on détaillera dans ce qui suit. Les seules différences importantes étaient la fréquence, le cache, et quelques détails dans le genre. Le Pentium 3 était une nouvelle microarchitecture qui ajoutait l'exécution dans le désordre. Un an et demi plus tard, le Pentium 4 est sorti et a été un échec. Ses performances étaient peu convaincantes face au Pentium 3, et sa consommation énergétique était très importante. La conséquence est que le Pentium 4 et le Pentium 3 ont survécu pendant un long moment, beaucoup de monde préférait acheter un Pentium 3. Intel a alors amélioré les microarchitectures du Pentium 3 et du 4, indépendamment, pendant environ 7 ans. La microarchitecture du Pentium 3 a subit plusieurs micro-évolutions, chacune avec une finesse de gravure différente, afin de satisfaire les consomateurs. Les premeirs Pentium 3 avaient une finesse de gravure de 250 nm, elle a chuté à 65 sur les derniers modèles. L'architecture du Pentium 4 a fait la même chose, pour tenter de corriger ses problèmes de performance et de consommation d'énergie. Les premiers Pentium 4 avaient uen finesse de gravure de 180 nm, elle a elle aussi chutée à 65 sur les derniers modèles. Après l'échec du Pentium 4, les ingénieurs d'Intel ont repris l'architecture P6 et l'ont améliorée fortement, pour donner l'architecture Core. Les micro-processeurs suivants ont fait évoluer cette architecture progressivement, au point où elle ne ressemble plus à l'originale. L'architecture Core a laissé la place à l'architecture Nehalem, puis Sandy Bridge, puis Haswell, puis Skylake, puis Ice Lake, et Golden Cove. Il s'agit de la lignée principale, partant du Pentium 3 et continuant jusqu'à nos jours. Ces microarchitectures ont suivi un motif assez simple, appelé modèle '''tick-tock'''. Chaque microarchitecture était déclinée en deux versions, la seconde ayant une finesse de gravure réduite. En parallèle, Intel a travaillé sur des processeurs basse performance et basse consommation, avec une microarchitecture très différente. Les processeurs Atom de microarchitecture Bonnel, pour être ensuite remplacés par les microarchitectures Silvermont, puis Goldmont et Gracemont. Ces microarchitectures ont évolué en parallèle de la lignée principale, il s'agit d'une lignée secondaire. Le tout est résumé dans ce schéma ci-dessous. [[File:IntelProcessorRoadmap-4v.svg|centre|vignette|upright=2.5|Roadmap des processeurs Intel, qui servira de structure pour la suite du chapitre]] De nos jours, les processeurs Intel utilisent les deux types de micro-architectures en même temps. Les CPU Intel modernes disposent de deux types de coeurs : les coeurs P et le coeurs E. Leur nom signifie "Performance" et "Efficient", qui trahissent leur but. Les coeurs P utilisent la microarchitecture de la lignée principale, qui est conçue pour la performance. Les coeurs E, quant à eux, utilisent les micro-architectures basse consommation de la lignée secondaire. ==Les processeurs x86 d'Intel, la lignée principale== Pour commencer, nous allons voir les processeurs Intel. N'y voyez pas du favoritisme derrière ce choix, la justification est toute autre. Si je commence par Intel, c'est pour commencer avec les Pentium 1 et 2, qui sont plus simples que tous les autres. Il s'agit en effet de processeurs superscalaires, mais sans exécution dans le désordre. L’absence d'exécution dans le désordre les rend bien plus simples à étudier que les autres, ce qui en fait un bon point de départ pour ce chapitre. Le successeur du Pentium 2 a intégré l'exécution dans le désordre, ce qui fait que les Pentium 1 et 2 sont les seuls processeurs superscalaires ''in-order''. Pour la concurrence, AMD n'a pas produit de processeurs ''in-order'', tous les processeurs produits par AMD intègrent l'exécution dans le désordre. A la rigueur, il y a bien les processeurs Atom de la lignée secondaire d'Intel. Cependant, même s'ils sont bien des CPU ''in-order'', leur architecture est assez compliquée. En comparaison, le Pentium est une vieille architecture, qui se débrouillait avec peu de transistors et était donc bien plus simple que celle de l'Atom. ===Le Pentium 1/MMX et les pipelines U/V=== Le processeur Pentium d'Intel avait un pipeline de 5 étages : un étage de chargement/prédiction de branchement, deux étages de décodage, un étage d'exécution et un dernier étage pour l'écriture dans les registres. Le Pentium 1 était un processeur double émission, intégrant deux pipelines nommés U et V. Chose importante, les deux pipelines n'étaient pas identiques. Le pipeline U pouvait exécuter toutes les instructions, mais le pipeline V était beaucoup plus limité. Par exemple, seul le pipeline U peut faire des calculs flottants, le pipeline V ne fait que des calculs entiers et des branchements. Les deux pipelines disposaient d'une unité de calcul entière, identique dans les deux pipelines. Mais le pipeline U incorporait un circuit multiplieur/diviseur et d'un ''barrel shifter''. L'unité flottante était sur le port d'émission du pipeline U, idem pour l'unité de calcul vectoriel MMX sur le Pentium MMX. Les deux pipelines avaient chacun une unité de calcul d'adresse, mais ils n'étaient pas identiques : celle du pipeline V ne gérait que l’instruction LEA, celle du pipeline U gérait tous les calculs d'adresse. {|class="wikitable" |- ! Pipeline U ! Pipeline V |- | ALU entière | ALU entière |- | Multiplieur/diviseur | |- | ''Barrel Shifter'' | |- | AGU complexe | AGU simple (opération LEA) |- | FPU | |- | Unité SIMD | |} Les deux pipelines géraient les opérations bit à bit, les additions, les soustractions et les comparaisons. Les autres instructions ne sont exécutables que dans le pipeline U. Pour être plus précis, les deux pipelines supportaient les instructions suivantes, ce qui fait qu'on pouvait en faire deux en même temps : * Les instructions arithmétiques INC, DEC, ADD, SUB ; * l'instruction de comparaison CMP ; * les instructions bit à bit AND, OR, XOR ; * l'instruction de calcul d'adresse LEA ; * l'instruction MOV (dépend du mode d'adressage) ; * les instructions de gestion de la pile PUSH et POP (dépend du mode d'adressage) ; * l'instruction NOP, qui ne fait rien. Il faut noter qu'il y a cependant quelques restrictions, beaucoup de paires d'instructions sont interdites. La plupart interdisent au pipeline V de faire quoique ce soit quand une opération particulière est émise dans le pipeline U. Par exemple, si le pipeline U exécute une multiplication ou une division, le processeur ne peut pas exécuter une opération dans le pipeline V. Et c'est pareil avec les branchements : si un branchement est émis dans le pipeline U, l'instruction suivant le branchement n'est pas émise dans le pipeline V, pour éliminer les dépendances de contrôle. De même, si le pipeline U exécute une opération flottante, le pipeline V ne pourra rien exécuter. La seule exception est l'instruction FCXH, qui échange deux registres flottants. [[File:Intel Pentium arch.svg|centre|vignette|upright=2.5|Microarchitecture de l'Intel Pentium MMX. On voit que certaines unités de calcul sont dupliquées.]] Un choix assez intéressant a été fait pour le cache de données. Nous avions vu dans le chapitre sur les CPU superscalaires que la superscalarité a un impact sur l'unité mémoire. Il y a alors deux implémentations. La première ne fait rien, l'unité mémoire ne change pas, et le processeur ne peut pas faire deux accès mémoire simultanés. La seconde duplique l'unité mémoire et les ports de lecture/écriture du cache, ce qui autorise des accès mémoire simultanés. Le Pentium 1 utilise une solution intermédiaire. Les ingénieurs d'Intel étaient partis à la base sur un cache totalement double port, pour obtenir des performances maximales. Mais diverses simulations et observations les ont fait changer d'avis. Les simulations ont montré qu'il est "rare" que les deux pipelines aient besoin de lire/écrire dans le cache en même temps. Et ils ont optimisé le cache de donnée pour en tenir compte. Le cache de données est partiellement multiport : simple port sur certains aspects, double port sur d'autres. Le cache est un cache splité, à savoir que les données et les ''tags'' sont séparés dans des mémoires séparées. La mémoire pour les ''tags'' est multiport, ce qui permet d'interroger les ''tags'' du cache deux fois par cycle. Un port est relié au pipeline u, un autre au pipeline V. Mais pour les données, le cache n'a qu'un seul port pour lire/écrire des données. Impossible donc de lire deux données en même temps, pour alimenter les deux pipelines. Il utilise cependant 8 banques permet d'accélérer des accès mémoire proches dans le temps, mais dans des cycles d'horloge différents. Entre les deux mémoires, il y a un circuit qui détecte les conflits, à savoir les situations où les deux pipelines accèdent en même temps au cache. S'ils veulent lire/écrire une donnée dans le même cycle, ce qui est impossible avec un seul port, le pipeline U a la priorité et le pipeline V attend le cycle suivant. Le circuit détecte aussi les conflits de banque, à savoir quand un pipeline accède à une banque en cours d'accès par l'autre pipeline (on rappelle qu'un accès au cache prend plusieurs cycles). Le circuit détecte aussi certaines dépendances mémoires, à savoir des accès consécutifs à la même adresse. : La TLB du processeur est aussi totalement double port. ===La microarchitecture P6 du Pentium 2/3=== Le Pentium 3 utilisait la '''microarchitecture P6''', qui a été dérivée dans de nombreuses variantes, dont les finesses de gravure n'étaient pas les mêmes. Il introduit une exécution dans le désordre simple, avec une fenêtre d'instruction centralisée, avec renommage dans le désordre dans le ROB (tampon de ré-ordonnancement), commandé par une table d'alias. C'était un processeur triple émission, soit une instruction de plus que la double émission du Pentium 1. Le pipeline passe de 5 étage sur le Pentium à 14 - 12 étages, dont le détail est le suivant : * Prédiction de branchement, deux cycles ; * Chargement des instructions, trois cycles ; * Décodage de l'instruction, deux cycles ; * Renommage de registre, un cycle ; * Copie des opérandes dans le tampon de ré-ordonnancement (lié au renommage de registre dans le ROB) ; * Dispath dans ou depuis la station de réservation. * Exécution de l'instruction ; * Écriture du résultat dans le ROB ; * Écriture dans le banc de registre physique. Les instructions sont chargées par blocs de 16 octets, avec un système de fusion de blocs pour gérer les instructions à cheval sur deux blocs. Lors d'un branchement, deux blocs doivent être chargés si l'instruction de destination n'est pas alignée sur 16 octets et cela cause un délai de un cycle d'horloge. Le décodage des instructions x86 était géré par plusieurs décodeurs. Il y avait trois décodeurs : deux décodeurs simples, et un décodeur complexe. Les décodeurs simples décodaient les instructions les plus fréquentes, mais aussi les plus simples, qui étaient décodées en une seule micro-opération. Les instructions CISC complexes étaient gérées uniquement par le décodeur complexe, basé sur un microcode, qui pouvait fournir jusqu'à 4 micro-opérations par cycle. Le tout est résumé avec la règle 4-1-1. La toute première instruction chargée depuis la file d'instruction va dans le premier décodeur simple. Si jamais le décodeur ne peut pas décoder l'instruction, l'instruction est redirigée dans un autre décodeur, avec un délai d'un cycle d'horloge. Les stations de réservations étaient regroupées dans une structure centralisée, en sortie de l'unité de renommage. Elles avaient 5 ports d'émission, qui étaient sous-utilisés en pratique. Niveau ALU, on trouve deux ALUs entières, une flottante, une unité pour les instructions SSE et autres, et trois unités pour les accès mémoire (regroupées en une seule unité dans le schéma ci-dessous). Les unités mémoire regroupent une unité de calcul d'adresse pour les lectures, une autre pour les écritures, et une unité pour la gestion des données à écrire. Les unités de calcul d'adresse sont des additionneurs à 4 opérandes, complétement différents des ALU entières. Les ALU entières sont deux unités asymétriques : une ALU simple, et une ALU complexe incorporant un multiplieur. Les deux peuvent exécuter des opérations d'addition, soustraction, comparaison, etc. [[File:P6 func diag.png|centre|vignette|upright=2|P6 func diag]] Les premiers Pentium 3 n'avaient pas de cache L2 dans le processeur, celui-ci était sur la carte mère. Mais il a été intégré dans le processeur sur la seconde version du Pentium 3, la version Coppermine. Le Pentium 3 a servi de base aux microarchitectures d'Intel qui ont suivi. Les changements à chaque nouvelle génération sont assez mineurs : la prédiction de branchement est améliorée, la taille des stations de réservation et du ROB augmente, idem avec les autres structures liées à l'exécution dans le désordre. Les processeurs Intel ont conservé une fenêtre d'instruction centralisée, alors qu'AMD utilise une autre méthode, comme nous allons le voir dans ce qui suit. Les seuls changements notables sont est le passage à un renommage dans le ROB à un renommage à banc de registre physique, ainsi que l'introduction du cache de micro-opération. Et ce sont des modifications qu'AMD a aussi faites, celle-ci étant clairement une bonne idée pour toutes les micro-architectures avec un budget en transistor suffisant. Il est intéressant de garder cela en tête, car une bonne partie des améliorations de chaque micro-architecture proviendra de là. ===La microarchitecture Core=== La '''microarchitecture Core''' fait suite au Pentium 4, mais reprend en fait beaucoup d’éléments du Pentium 2 et 3. Elle utilise la station de réservation unique avec renommage dans le ROB, provenant du Pentium 2/3. Elle supporte aussi les optimisations des opérations ''load-up'', avec notamment un support des macro-opérations mentionnées plus haut. Les améliorations sont assez diverses, mais aussi assez mineures. * Le processeur incorpore un cache L2, en plus des caches L1 déjà présents auparavant. * La prédiction de branchement a été améliorée avec notamment l'ajout d'une ''Fetch Input Queue''. * L'architecture Core passe à la quadruple émission, soit une instruction de plus que sur le Pentium 2 et 3. Pour cela, un quatrième décodeur est ajouté, il s'agit d'un décodeur simple qui ne fournit qu'une seule micro-opération en sortie. * Un ''stack engine'' et un ''Loop Stream Detector'' ont été ajoutés, ainsi que le support de la macro-fusion qui fusionne une instruction de test et le branchement qui suit en une seule micro-opération. * Les techniques de désambiguïsation mémoire sont implémentées sur cette micro-architecture. Il y a quelques modifications au niveau de l'unité de chargement. La file d'instruction a toujours ce système de fusion de blocs, sauf que les branchements ne causent plus de délai d'un cycle lors du chargement. La file d'instruction est suivie par un circuit de prédécodage qui détermine la taille des instructions et leurs frontières, avant de mémoriser le tout dans une file de 40 instructions. La station de réservation dispose de 6 ports d'émission, mais on devrait plutôt dire 5. Sur les 5, il y en a un pour les lectures, un pour les écritures. Les deux sont reliées à une ''Load/Store Queue'', appelée ''Memory Ordering Buffer''. Elle est elle-même reliée au cache de données par deux ports : un port de lecture et un port d'écriture. Les trois ports d'émission restants sont connectés aux unités de calcul. Les trois ports récents sont chacun reliés à une ALU entière. L'additionneur flottant est connecté au port 1, alors que le multiplieur/diviseur flottante est connecté au port 0. Le fait de mettre les deux sur des ports différents permet d'émettre une addition et une multiplication flottant simultanément. Le multiplieur entier est relié au second port d'émission, celui sur lequel se trouve l'additionneur flottant. Le résultat que le processeur peut émettre un mix d'opérations flottantes et entière assez varié. [[Image:Intel Core2 arch.svg|centre|vignette|upright=2|Intel Core microarchitecture]] ===Les microarchitectures Sandy Bridge and Ivy Bridge=== Les micro-architectures suivant la micro-architecture Core ont introduit quelques grandes modifications : le passage à un renommage à banc de registre physique, l'ajout d'un cache de micro-opérations (et d'un ''Loop Stream Detector''). L'ajout du cache de micro-opérations est un gros changement, particulièrement avec le jeu d’instruction x86. Le décodage des instructions est lent, couteux en énergie. Mais avec l'introduction du cache de micro-opération, la majorité des micro-opérations est non pas décodée, mais lue depuis le cache de micro-opérations. Les décodeurs décodent les instructions pas encore exécutées, mais les exécutions suivantes sont lues depuis le cache de micro-opérations. Et vu la grande présence de boucles, le cache de micro-opérations est l'alimentation principale du pipeline. Les décodeurs servent surtout à alimenter le cache de micro-opérations, parfois décoder quelques instructions isolées exécutées de-dehors de boucles, pas plus. Concrètement, ils servent pour 10 à 20% des micro-opérations exécutées. Intel a d'ailleurs reflété ce fait dans sa terminologie. Intel distingue deux voies de chargement : le ''legacy pipeline'' et le cache de micro-opérations. L'unité de chargement et les décodeurs sont regroupés dans la voie du ''legacy pipeline''. Le cache de micro-opérations est complété avec un ''Loop Stream Detector'', placé après le cache en question. Les décodeurs et le cache de micro-opérations alimentent une file de micro-opérations, située juste avant l'étage de renommage de registres. La file de micro-opérations sert en quelque sorte de tampon entre l'étage de "décodage" et celui de renommage. Le ''Loop Stream Detector'' utilise cette file de micro-opérations comme d'un cache lorsqu'une boucle est détectée. Les micro-opérations de la boucle sont lue depuis la file de micro-opérations, pour être envoyée au renommeur de registres. L'avantage est que le cache de micro-opérations et/ou les décodeurs sont mis en pause et clock-gatés lorsqu'une boucle s'exécute, ce qui réduit la consommation du processeur. Le ''Loop Stream Detector'' et le cache de micro-opération ont globalement le même effet : désactiver tout ce qui est avant, le ''Loop Stream Detector'' appliquant cette méthode au cache de micro-opération lui-même.. Voyons maintenant quelles sont les micro-architectures qui implémentent ces optimisations. Les microarchitectures '''Sandy Bridge''' and '''Ivy Bridge''' sont similaires à l'architecture Core, si ce n'est pour le passage à un renommage à banc de registre physique, et l'ajout d'un cache de micro-opérations. Le nombre de ports d'émission passe à 7, avec 4 pour les instructions arithmétiques (flottantes comme entière), 2 pour les lectures, et un pour les écritures (en fait deux, avec un pour le calcul d'adresse, l'autre pour la donnée à écrire). Pour le reste, rien ne change si ce n'est la prédiction de branchement Les architectures '''Haswell''' et '''Broadwell''' ont ajouté quelques unités de calcul, élargit la sortie du cache de micro-opérations. Un port d'émission pour opération entières a été ajouté, de même qu'un port pour les accès mémoire. Le processeur passe donc à 8 ports d'émission, ce qui permet d'émettre jusqu'à 8 micro-opérations, à condition que le cache de micro-opération suive. Pour le reste, le processeur est similaire aux architectures précédentes, si ce n'est que certaines structures grossissent. L'architecture '''Skylake''' réorganise les unités de calcul et les ports d'émission pour gagner en efficacité. Pour le reste, les améliorations sont mineures. A la rigueur, l'unité de renommage de registre ajoute des optimisations comme l'élimination des MOV, les idiomes liés aux opérations avec zéro, etc. ===Les microarchitectures récentes d'Intel=== Les architectures '''Ice Lake''' et '''Tiger Lake''' passent de quadruple émission à la pentuple émission. Par contre, le processeur utilise toujours 4 décodeurs. Mais les micro-opérations étant émises depuis le cache de micro-opérations, ce n'est pas un problème pour la pentuple émission. Le processeur peut parfaitement émettre 5 micro-opérations en même temps, si elles sont lues depuis le cache de micro-opérations. Là encore, on voit à quel point le cache de micro-opération découple ce qu'il y avant de ce qu'il y a après. La microarchitecture '''Golden Cove''' altère les décodeurs et l'unité de chargement. Sur toutes les générations précédentes, on reste sur une unité de chargement qui charge 16 octets à la fois et il y a toujours 4 décodeurs identiques aux générations précédentes. Golden Cove passe à 6 décodeurs simples, et double la taille du chargement qui passe à 32 octets. Une telle stagnation sur les unités de chargement et de décodage s'explique encore une fois par la présence du cache de micro-opération fait que ce n'est pas trop un problème. Tout ce qui précède le cache de micro-opérations n'a pas de raison d'évoluer, car ce cache est très puissant. Niveau unités de calcul, le CPU a pas moins de 5 ALU entières, deux ''barrel shifters'', un multiplieur et deux unités de branchements. Le tout est répartit sur 5 ports d'émission. Pour les unités mémoire, il y a trois unités LOAD pour les lectures et deux unités STORE pour les écritures. Il n'y a plus d'unité flottante proprement dite, mais une unité SIMD qui est capable de faire plusieurs calculs flottants, qu'on ne détaillera pas ici, car nous n'avons pas encore vu les techniques de SIMD. Notons qu'il y a trois fenêtres d'instruction séparées pour : les instructions entières/flottantes, les lectures, les écritures. Le processeur peut décoder 6 instructions par cycle, et en émettre 6 à destination des fenêtres d'instructions. Les fenêtres d'instructions peuvent émettre 5 instructions entières, trois lectures et deux écritures en même temps. Le ROB accepte de ''commit'' 8 µops par cycle. [[File:Golden Cove.png|centre|vignette|upright=3|Golden Cove]] Il s'agit donc de processeurs superscalaires très larges, ce qui est la norme de nos jours et le restera pendant longtemps. Et il faut noter que même les cœurs basse performance sont dans ce cas. Pour rappel,n les CPU Intel modernes regroupent deux types de cœurs : les cœurs P et les cœurs E. Les cœurs P sont des cœurs haute performance, basés sur les microarchitectures Cove, optimisées pour la performance. Les cœurs E, quant à eux, utilisent une microarchitecture différente, conçue pour économiser de l'énergie et avoir une consommation réduite, au prix de performances réduites. Pourtant, même les coeurs E utilisent une superscalarité large ! Par exemple, voici ce que donne la microarchitecture Gracemont. Elle peut décoder deux paquets de 3 instructions chaque. Elle envoie ensuite 5 µops à l'unité d'émission, qui sont distribuées dans deux pipelines : un pipeline flottant/SIM et un pipeline entier. Et le pipeline entier peut émettre 4 µops entières, deux lectures, deux écritures et deux branchements ! Pire que ça, le ''barrel shifter'' et le multiplieur/diviseur sont dupliqués en deux exemplaires ! [[File:Gracemont.png|centre|vignette|upright=3|Gracemont]] La différence entre les microarchitectures P et E tient dans le système d'exécution dans le désordre. Le ROB, les fenêtres d'instructions, les différentes files de µops et caches : tout est plus petit sur les coeurs E. De plus, les coeurs E utilisent des files d'instructions pour les opérations mémoire et flottantes, au lieu de fenêtres d'instruction. Le schéma ci-dessous illustre ce qu'il en est sur l'architecture Crestmont, pour coeur sE. On voit que les files pour µops flottantes et mémoire sont de type ''non-scheduling'', ce qui veut dire que ce sont de simples files d'instruction. Les performances mémoire et flottantes sont donc un peu réduites. [[File:Crestmont.png|centre|vignette|upright=3|Crestmont]] ==Un étude des microarchitectures superscalaires x86 d'AMD== Les architectures Intel ont évolué progressivement, sans grandes cassure. Il y a une continuité presque initerrompue entre l'architecture du Pentium 2 et les architectures modernes. Intel a fait des améliorations mineures à chaque nouvelle micro-architecture, si on omet le passage à un renommage à banc de registre physique et l'ajout du cache de micro-opération. A l'opposé, les architectures AMD ont eu de nombreuses cassures dans la continuité où AMD a revu sa copie de fond en comble. Étudier ces architectures demande de voir trois choses séparément : le ''front-end'' qui regroupe l'unité de chargement et les décodeurs, le ''back-end'' qui gère l'exécution dans le désordre et les unités de calcul, et le sous-système mémoire avec les caches et la ''Load Store Queue''. Leur étude sera plus ou moins séparée dans ce qui suit, pour chaque classe d'architecture. ===La première génération de CPU AMD : les architectures K5, K6, K7, K8 et K10=== La première génération de processeurs AMD est celle des architectures K5, K6, K7, K8 et K10. Il n'y a pas de K9, qui a été abandonné en cours de développement. Les processeurs K5 et K6 portent ce nom au niveau commercial. Par contre, les processeurs d'architecture K7 sont aussi connus sous le nom d''''AMD Athlon''', les AMD K8 sont connus sous le nom d''''AMD Athlon 64''', et les architecture K10 sont appelées les '''AMD Phenom'''. Comme le nom l'indique, l'architecture K8 a introduit le 64 bits chez les processeurs AMD. Elles ont une architecture assez similaire pour ce qui est du chargement et des caches. Toutes disposent d'au minimum un cache L1 d'instruction et d'un cache L1 de données. Le K5 n'avait que ces caches, mais un cache L2 a été ajouté avec le K7, puis un L3 avec le K10. L'AMD K5 avait une TLB unique, mais les processeurs suivants avaient une TLB pour le L1 d'instruction et une autre pour le L1 de données. Idem pour le cache L2, avec deux TLB : une pour les données, une pour les instructions. Les caches L1/L2 sont de type exclusifs, à savoir que les données dans le L1 ne sont pas recopiées dans le L2. Le cache L2 est précisément un cache de victime, qui mémorise les données/instructions, évincées des caches L1 lors du remplacement des lignes de cache. L'introduction du cache L2 a entrainé l'ajout de deux TLB de second niveau : une L2 TLB pour les données et une autre pour les instructions. Les architectures K8 et K10 ont ajouté un cache L3, avec un accès indirect à travers l'interface avec le bus. : L'AMD K7 originel, aussi appelée Athlon classique, n'avait pas de cache L2, mais celui-ci était placé sur la carte mère et fonctionnait à une fréquence moitié moindre de celle du CPU. L'Athlon Thunderbird, puis l'Athlon XP, ont intégré le cache L2 dans le processeur. {|class="wikitable" |- ! Architecture AMD ! colspan="5" | Caches |- | rowspan="2" | K5 | L1 instruction || L1 données || colspan="3" | |- | colspan="2" | TLB unique || colspan="3" | |- | colspan="4" | |- | rowspan="2" | K6 | L1 instruction || L1 données || colspan="3" | L2 unifié |- | TLB L1 instruction || TLB L1 données || colspan="3" | |- | colspan="6" | |- | rowspan="2" | K7, K8 | L1 instruction || L1 données || colspan="2" | L2 unifié || |- | TLB L1 instruction || TLB L1 données || TLB L2 instruction || TLB L2 données || |- | colspan="6" | |- | rowspan="2" | K10 | L1 instruction || L1 données || colspan="2" | L2 unifié || L3 |- | TLB L1 instruction || TLB L1 données || TLB L2 instruction || TLB L2 données || |} Fait important, les architectures K5 à K10 utilisent la technique du '''prédécodage''', où les instructions sont partiellement décodées avant d'entrer dans le cache d'instruction. Le prédécodage facilite grandement le travail des décodeurs d'instruction proprement dit. Par contre, le prédécodage prend de la place dans le cache L1 d'instruction, une partie de sa capacité est utilisé pour mémoriser les informations prédécodées. C'est donc un compromis entre taille du cache et taille/rapidité des décodeurs d'instruction. Sur les architectures K5 et K6, le prédécodage précise, pour chaque octet, si c'est le début ou la fin d'une instruction, si c'est un octet d'opcode, en combien de micro-opérations sera décodée l'instruction, etc. A partir de l'AMD K7, le prédécodage reconnait les branchements inconditionnels. Lorsqu'un branchement inconditionnel est pré-décodé, le pré-décodage tient compte du branchement et continue le pré-décodage des instructions à partir de la destination du branchement. Le système de prédécodage est abandonnée à partir de l'architecture Bulldozer, qui suit l'architecture K10. La prédiction de branchement de ces CPU tire partie de ce système de pré-décodage, à savoir que les prédictions de branchement sont partiellement mémorisées dans les lignes de cache du L1 d'instruction. Par exemple, l'AMD K5 se passe de ''Branch Target Buffer'' grâce à cela. Si une ligne de cache contient un branchement, elle mémorise l'adresse de destination de ce branchement, en plus des bits de pré-décodage. Si il y a plusieurs branchements dans une ligne de cache, c'est l'adresse de destination du premier branchement pris dans cette ligne de cache qui est mémoirsée. Un défaut de cette approche est que si le branchement n'est pas dans le L1 d'instruction, aucune prédiction de branchement ne peut être faite et le préchargement ne peut pas fonctionner. C'est une limitation que n'ont pas les BTB découplées du cache L1 : elles peuvent prédire un branchement qui a été évincé dans le L2 ou le L3, tant que l'entrée associée est dans le BTB. Les prédictions peuvent même servir à précharger les instructions utiles. [[File:Comparaison du chargement de l'AMD K5 et K6.png|centre|vignette|upright=2|Comparaison du chargement de l'AMD K5 et K6]] Au niveau du décodage, on trouve de nombreuses différences entre les premières architectures AMD. L'AMD K5 contient 4 décodeurs hybrides, afin de décoder 4 instructions par cycles. Le K5 a quatre décodeurs simples couplés à 4 décodeurs complexes avec chacun un accès au micro-code. Une instruction peut donc passer par a donc deux voies de décodage : un décodage rapide et simple pour les instructions simples, un décodage lent et passant par le microcode pour les instructions complexes. Pour décoder 4 instructions, les deux voies sont dupliquées en 4 exemplaires, ce qui a un cout en circuits non-négligeable. L'AMD K6 utilise moins de décodeurs et ne peut que décoder deux instructions à la fois maximum. Par contre, il fournit en sortie 4 micro-opérations. Il intègre pour cela deux décodeurs simples, un décodeur complexe et un décodeur micro-codé. Un décodeur simple transforme une instruction simple en une ou deux micro-opérations. Il est possible d'utiliser les deux décodeurs simples en même temps, afin de fournir 4 micro-opérations en sortie du décodeur. Les deux autres décodent une instruction complexe en 1 à 4 micro-opérations. Si jamais la ou les deux instructions sont décodées en 1, 2 ou 3 micro-opérations, les micro-opérations manquantes pour atteindre 4 sont remplies par des NOPs. Pour le K7 et au-delà, le processeur dispose de décodeurs séparées pour les instructions micro-codées de celles qui ne le sont pas. Le processeur peut décoder jusqu’à 3 instructions par cycle. Le décodage d'une instruction microcodée ne peut pas se faire en parallèle du décodage non-microcodé. C'est soit le décodeur microcodé qui est utilisé, soit les décodeurs câblés, pas les deux en même temps. Le décodage d'une instruction prend 4 cycles. Les instructions non-microcodées sont décodées en une seule micro-opération, à un détail près : le CPU optimise la prise en charge des instructions ''load-up''. La différence entre le K6 et le K7 s'explique par des optimisations des instructions ''load-up''. Sur le K6, les instructions ''load-up'' sont décodées en deux micro-opération : la lecture en RAM, l'opération proprement dite. Mais sur le K7, une instruction ''load-up'' est décodée en une seule micro-opération. En conséquence, les décodeurs simples sont fortement simplifiés et le décodeur complexe disparait au profit d'un microcode unique. [[File:Décodage sur le K5 et le K5.png|centre|vignette|upright=3|Décodage sur le K5 et le K5]] ====Les microarchitectures K5 et K6 d'AMD==== Les deux premières architectures étaient les architectures K5 et K6, l'architecture K6 ayant été déclinée en quatre versions, nommées K6-1, K6-2, et K-3, avec une version K6-3 bis. Elles sont regroupées ensemble car elles ont beaucoup de points communs. Par exemple, tout ce qui a trait au chargement et au cache était similaire, de même que les unités de calcul. Les deux architectures avaient n'avaient pas de cache L2 et devaient se contenter d'un cache L1 d'instruction et d'un cache L1 de données. L'AMD K5 incorpore une TLB unique, alors que le K6 utilise des TLB séparées pour le cache d'instruction et le cache de données. Une différence entre l'architecture K5 et K6 est que la première utilise des caches normaux, alors que la seconde utilise des ''sector caches''. Les deux architectures disposaient des unités de calcul suivantes : deux ALU entières, une FPU, deux unités LOAD/STORE pour les accès mémoire, une unité de branchement et une ou plusieurs unités SIMD. Une organisation classique, donc. Pour les unités entières, il y avait deux ALU simples, un ''barrel shifter'' et un diviseur. Il n'y a pas d'erreur, le processeur incorpore un circuit diviseur, mais pas de circuit multiplieur. La raison est que la multiplication est réalisée par la FPU ! En effet, le multiplieur flottant de la FPU intègre un multiplieur entier pour multiplier les mantisses, qui est utilisé pour les multiplications entières. La même technique a été utilisée sur l'Atom, comme vu plus haut. Le tout était alimenté par deux ports d'émission, appelés ports X et Y. Sur l'architecture K5, le ''barrel shifter'' et le diviseur sont des ports différents. {|class="wikitable" |+ AMD K5 |- ! Port X ! Port Y |- | ALU simple | ALU simple |- | ''Barrel Shifter'' | Diviseur |} Sur l'architecture K6, le ''barrel shifter'' et le diviseur sont sur le même port. {|class="wikitable" |+ AMD K6 |- ! Port X ! Port Y |- | ALU simple | ALU simple |- | | ''Barrel Shifter'' |- | | Diviseur |} Niveau unités mémoire, le K5 avait deux unités LOAD/STORE, chacune capable de faire lecture et écriture. Par contre, la ''store queue'' n'a qu'un seul port d'entrée, ce qui fait que le processeur peut seulement accepter une écriture par cycle. Le processeur peut donc émettre soit deux lectures simultanées, soit une lecture accompagnée d'une écriture. Impossible d'émettre deux écritures simultanées, ce qui est de toute façon très rare. L'architecture K6 utilise quant à elle une unité LOAD pour les lectures et une unité STORE pour les écritures. Ce qui permet de faire une lecture et une écriture par cycle, pas autre chose. Niveau unités SIMD, l'architecture K7 n'avait qu'une seule unité SIMD, placée sur le port d'émission X. L'architecture K8 ajouta une seconde unité SIMD, sur l'autre port d'émission entier. De plus, trois ALU SIMD ont été ajoutées : un décaleur MMX, une unité 3DNow!, une unité mixte MMX/3DNow. Elles sont reliées aux deux ports d'émission entier X et Y ! Elles ne sont pas représentées ci-dessous, par souci de simplicité. [[File:Unité de calcul des processeurs AMD K5 et K6.png|centre|vignette|upright=2|Unité de calcul des processeurs AMD K5 et K6. les unités sur la même colonnes sont reliées au même port d'émission.]] Si les unités de calcul et le chargement sont globalement les mêmes, les deux architectures se différencient sur l'exécution dans le désordre. L'AMD K5 utilise du renommage de registre dans le ROB avec des stations de réservation. Par contre, l'AMD K6 utilise une fenêtre d'instruction centralisée. De plus, son renommage de registre se fait avec un banc de registre physique. L'architecture AMD K5 utilisait de deux stations de réservation par unité de calcul, sauf pour les deux unités mémoire partageaient une station de réservation unique (deux fois plus grande). Les stations de réservation sont cependant mal nommées, vu que ce sont en réalité des mémoire FIFO. Une micro-opération n'est émise que si elle est la plus ancienne dans la FIFO/station de réservation. Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique. Le tampon de ré-ordonnancement faisait seulement 16 instructions. [[File:AMD K5.jpg|centre|vignette|upright=3|AMDK5 Diagramme.]] L'architecture K6 remplace les stations de réservations par une fenêtre d'instruction centralisée. Les 4 micro-opérations renommées sont écrites dans la fenêtre d'instruction par groupe de 4, NOP de ''padding'' inclus. La fenêtre d'instruction centralisé contient 24 micro-opérations, groupées en 6 groupes de 4 micro-opérations, avec potentiellement des NOP dedans suivant le résultat du décodage. L'avantage est que l'implémentation de la fenêtre d'instruction est simple. La fenêtre d'instruction centralisée permettait d'émettre 6 micro-opérations en même temps (une par unité de calcul/mémoire). Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique. Le processeur utilisait un renommage avec un banc de registre physique. Le banc de registre physique pour les entiers contenait 48 registres, dont 24 étaient des registres architecturaux et 24 étaient des registres renommés. Sur les 24 registres architecturaux, 16 avaient une fonction de ''scratchpad'' que les ''datasheets'' d'AMD ne détaillent pas, les 8 restants étaient les registres généraux EAX, EBX, etc. [[File:AMD K6 Little foot & Modl 6.png|centre|vignette|upright=3|AMD K6 original.]] ====Les microarchitectures K7, K8 et K10 d'AMD==== Les microarchitectures suivantes sont les architectures K7, K8 et K10. Les architectures K7, K8 et K10 sont assez similaires. La différence principale entre le K7 et le K8 est le support du 64 bits. Les apports du K10 sont la présence d'un cache L3, d'une unité de calcul supplémentaire et d'améliorations de la prédiction de branchement. La taille de certains caches a été augmentée, de même que la largeur de certaines interconnexions/bus. L'architecture K7 des processeurs Athlon utilisait le renommage de registre, mais seulement pour les registres flottants, pas pour les registres entiers. Le ranommeg des registres flottants étaient réalisé via un banc de registres physique, ne contenant que des registres flottants. Les architectures K8 et K10 utilisent le renommage de registres pour tous les registres, entiers comme flottants. Par contre, le renommage de registre n'est pas réalisé de la même manière pour les registres entiers et flottants. Les registres entiers sont renommés dans le tampon de ré-ordonnancement, comme c'était le cas sur les architectures Intel avant le Pentium 4. Par contre, les registres flottants sont renommés grâce à un banc de registre physique. Le K8 est donc un processeur au renommage hybride, qui utilise les deux solutions de renommage principales. A partir du K7, le CPU optimise la prise en charge des instructions ''load-up''. Les instructions ''load-op'' sont appelées des macro-opérations dans la terminologie d'AMD, et aussi d'Intel. L'idée est que les instructions ''load-up'' sont décodées en micro-opérations intermédiaires. Elles sont propagées dans le pipeline comme étant une seule micro-opération, jusqu'à l'étage d'émission. Lors de l'émission, les instructions ''load-up'' sont scindées en deux micro-opérations : la lecture de l'opérande, puis l'opération proprement dite. Faire ainsi économise des ressources et optimise le remplissage du tampon de ré-ordonnancement, des fenêtres d'instructions, des stations de réservation, etc. Le tampon de réordonnancement est combiné avec divers circuits en charge de l'exécution dans le désordre, dans ce qui s'appelle l'''instruction control unit''. Il contient de 72 à, 84 instructions, qui sont regroupées en groupes de 3. Là encore, comme pour le K5 et le K6, le tampon de réordonnancement tient compte de la sortie des décodeurs. Les décodeurs fournissent toujours trois micro-opérations par cycle, quitte à remplir les vides par des NOP. Le tampon de réordonnancement reçoit les micro-opérations, NOP inclus, par groupes de 3, et est structuré autour de ces triplets de micro-opération, y compris en interne. Pour ce qui est de l'unité mémoire, elle est précédée par une file de µops mémoire, qui émet les accès mémoire dans l'ordre du programme. Elle est souvent qualifiée de ''Load-Store Queue'', mais ce n'est pas la terminologie que nous utilisons dans ce cours. La file de micro-opération lire/écrire 64 bits par cycle depuis le cache L1, ce qui fait un seul accès au cache par cycle. La file de µops mémoire est appelée la ''Pre-Cache Queue''. Si au vu de son nom, vous avez deviné qu'il y avait une ''Post-Cache Queue''. Elle mémorise les lectures/écritures émises, mais qui ont levé un défaut de cache L1. Elle ne fait pas partie de la file de µops mémoire proprement dite. Les architectures K7, K8 et K10 ont des unités de calcul très similaires. Concrètement, il y a trois ALU entières, trois unités de calcul d'adresse, et une FPU. Le processeur incorpore, aussi un multiplieur entier, relié sur le port d'émission de la première ALU. La FPU regroupe un additionneur flottant, un multiplieur flottant, et une troisième unité LOAD/STORE pour les lectures/écritures pour les nombres flottants. L'architecture K8 ajoute une unité de manipulation de bit, la K10 un diviseur entier. [[File:Unité de calcul des processeurs AMD K7, K8 et K10.png|centre|vignette|upright=2|Unité de calcul des processeurs AMD K7, K8 et K10]] La manière d'alimenter les ALU en micro-opérations varie un petit peu entre les architectures K7, K8 et K10. Il y a cependant quelques constantes entre les trois. La première est qu'il y a une fenêtre d'instruction séparée pour les flottants, de 36 à 42 entrées, avec renommage de registre. La fenêtre d'instruction flottante a trois ports d'émission : un pour l'additionneur flottant, un autre pour le multiplieur, et un troisième pour la troisième unité flottante qui s'occupe du reste. La seconde est que chaque ALU entière est couplée avec une unité de calcul d'adresse. Par contre, la méthode de couplage varie d'un processeur à l'autre. : Les stations de réservation sont nommées des ''schedulers'' dans les schémas qui suivent. La microarchitecture K7 avait deux fenêtres d'instruction : une pour les opérations flottantes, une autre pour les instructions entières et les accès mémoire. La fenêtre d'instruction entière était reliée à 3 ALU entières et à 3 AGU. Elle pouvait émettre trois micro-opérations en même temps : trois micro-opérations entières, trois micro-opérations mémoire. Les AGU étaient reliées à la file de µops mémoire mentionnée plus haut, ce qui permet d'émettre trois µops mémoire par cycle. Par contre, la file de µops mémoire ne pouvait exécuter qu'une lecture de 64 bits ou une écriture de 64 bits. En clair, trois micro-opérations mémoire peuvent être émises par cycle, cela entraine trois calculs d'adresse simultanés, mais les trois lectures/écritures sont mises en attente dans la file de µops mémoire. Elles s'exécutent alors l'une après l'autre. La fenêtre d'instruction entière contenait 5 à 6 groupes de 3 macro-opérations. Vous noterez que j'ai parlé de macro-opérations et pas de micro-opérations, car les instructions ''load-up'' sont considérées comme une seule "micro-opération" dans la fenêtre d'instruction entière. Et cela se marie bien avec une fenêtre d'instruction unique partagée entre pipeline entier et pipeline mémoire. Une macro-opération était scindée en deux micro-opérations : une micro-opération mémoire et une micro-opération entière. Il est donc avantageux de regrouper unités mémoire et unités entières à la même fenêtre d'instruction pour ce faire. [[File:AMD K7.png|centre|vignette|upright=3|AMD K7]] Sur les architectures K8 et K10, la station de réservation unique de 15 micro-opérations est remplacée par trois stations de réservations, de 8 micro-opérations chacune pour le K8, de 10 pour le K10. Chaque station de réservation entière alimente une unité de calcul entière et une unité de calcul d'adresse. l'unité de calcul d'adresse est reliée à la file de µops mémoire, qui n’exécute toujours qu'un seul accès mémoire par cycle. Le multiplieur est relié à la première station de réservation, sur le même port d'émission que l'ALU. [[File:AMD Husky microarchitecture.png|centre|vignette|upright=3|AMD Husky microarchitecture]] La microarchitecture K10 a été déclinée en plusieurs versions, nommées Grayhound, Grayhound+ et Husky, Husky étant une architecture gravée en 32 nm dédiée aux processeurs A-3000. L'architecture Grayhound a plus de cache et un ROB plus grand, la Husky est quand à elle un peu plus différente. Elle n'a pas de cache L3, contrairement aux autres architectures K10, ce qui simplifie fortement son sous-système mémoire. Par contre, les fenêtres d'instructions/stations de réservation et le ROB sont plus grands, pareil pour les files dans l'unité mémoire. Une ALU pour les divisions entières a aussi été ajoutée. Pour résumer, les architectures K7, K8 et K10 séparent les pipelines entiers et flottants : trois pipelines entiers avec chacun son unité de calcul, et un pipeline flottant avec plusieurs unités de calcul. Les raisons à cela sont assez diverses. Disons que dupliquer des ALU entières simples prend peu de transistors, là où les gros circuits comme le multiplieur ou la FPU ne sont pas dupliqués. Et cela a un autre avantage : le renommage, ''dispatch'' et l'émission sont plus simples. Les pipelines entiers ont une exécution dans le désordre peu complexe, grâce au grand nombre d'unités de calcul, ce qui fait que le pipeline entier est de seulement 15 cycles au total (chargement et décodage inclus). A l'opposé, la FPU est alimentée par une exécution dans le désordre très complexe, avec banc de registre physique et beaucoup de ressources, mais au prix d'un pipeline flottant plus long de 3 cycles, soit 18 cycles au total. ===Les microarchitectures ZEN d'AMD=== Viennent ensuite les '''microarchitectures Bulldozer''', avec trois révisions ultérieures nommées Piledriver, Steamroller et Excavator. Mais du fait de l'utilisation de techniques de multithreading matériel que nous n'avons pas encore abordé, nous ne pouvons pas en parler ici. Les microarchitectures suivantes sont les '''architectures ZEN 1/2/3/4/5'''. Elles se ressemblent beaucoup, chacune accumulant les améliorations des précédentes. Mais le cœur de l'architecture reste plus ou moins le même. En passant à la suivante, le nombre de registre virtuel augmente, le ''branch target buffer'' augmente en taille, le ROB et les files d'attente grossissent, les caches de micro-opération aussi, les caches grossissent, etc. Une optimisation intéressante est l'ajout d'un cache de micro-opération, qui améliore grandement les performances du ''front-end'', notamment pour les boucles. La microarchitecture Zen 1 est illustrée ci-dessous. Comme on le voit, les registres flottants ont une unité de renommage séparée de celle pour les entiers, mais les deux utilisent du renommage à banc de registre physique. Il y a par contre une différence au niveau des fenêtres d'instruction, notées ''scheduler'' dans le schéma. Pour ce qui est des unités de calcul flottantes, il y a une fenêtre unifiée qui alimente quatre ALU, grâce à 4 ports d'émission. Mais pour les ALU entières, il y a une fenêtre d'instruction par ALU, avec un seul port d'émission connecté à une seule ALU. La raison de ce choix est que les opérations flottantes ont un nombre de cycle plus élevé, sans compter que les codes flottants mélangent bien additions et multiplication. Une fois décodées, les instructions sont placées dans une première file de micro-opérations om elles attendent, puis sont dispatchées soit dans le pipeline entier, soit dans le pipeline flottant. les micro-opérations entières sont insérées dans une fenêtre d'instruction directement, alors que les micro-opérations flottantes doivent patienter dans une seconde file de micro-opérations. La raison est que les micro-opérations flottantes ayant une grande latence, trop d'instructions flottantes consécutives pourraient bloquer le pipeline flottant, sa fenêtre d'instruction étant pleine. Le pipeline flottant étant bloqué, la première file de micro-opérations serait bloquée et on ne pourrait plus émettre de micro-opérations entières. Pour éviter cela, une solution serait d'agrandir la file de micro-opérations, mais cela la rendrait plus lente et se ferait donc au détriment de la fréquence d'horloge. Alors une solution a été d'ajouter une seconde file de micro-opérations, au lieu d'agrandir la première. [[File:Zen microarchitecture.svg|centre|vignette|upright=3|Microarchitecture Zen 1 d'AMD.]] Le passage à la microarchitecture n'a pas causé de grands changements. Le Zen 2 a ajouté une unité de calcul d'adresse, ce qui fait qu'on passe à 4 ALU, 3 AGU et 4 FPU. La fenêtre d'instruction flottante reste la même. Par contre, les fenêtres d'instruction entières changent un peu. Ou plutot devrais-je dire les fenêtres d'instruction mémoire. En effet, le Zen 2 fusionne les fenêtres d'instructions liées aux AGU en une seule fenêtre d'instruction deux fois plus grosse. Le Zen 5 a ajouté deux autres ALU entières et une unité de calcul d'adresse (6 ALU / 4 AGU) ==Les processeurs Atom d'Intel, de microarchitecture Bonnell== L'architecture de l'Atom première génération est assez simple. Son pipeline faisait 16 étages, ce qui est beaucoup. C'est un processeur 32 bits, ce qui aura son importance dans ce qui suit. Il était conçu pour être un processeur basse consommation, donc peu puissant. En conséquence, il n'a pas d'exécution dans le désordre, même s'il est superscalaire. C'était la norme à l'époque pour les processeurs basse consommation, que de faire sans exécution dans le désordre. De nos jours, les choses ont bien changée, même les processeurs basse consommation ont exécution dans le désordre, superscalarité et renommage de registres. ===Le ''front-end'' de l'Atom=== Le cache d'instruction permet de lire 8 octets par cycle, qui sont placés dans une file d'instruction, elle-même suivie par deux décodeurs. Le fait que les décodeurs lisent les instructions depuis une file d'instruction fait que les deux instructions décodées ne sont pas forcément consécutives en mémoire RAM. Par exemple, l'Atom peut décoder un branchement prédit comme pris, suivi par l'instruction de destination du branchement. Les deux instructions ont été chargées dans la file d'instruction et sont consécutifs dedans, alors qu'elles ne sont pas consécutives en mémoire RAM. Sur l'Atom, la majorité des instructions x86 sont décodées en une seule micro-opération, y compris les instructions ''load-up''. Le microcode n'est utilisé que pour une extrême minorité d'instructions et est à part des deux décodeurs précédents. L'avantage est que cela permet d'utiliser au mieux la file de micro-opération, qui est de petite taille. Mais surtout, cela permet de grandement réduire la consommation du processeur, au détriment de ses performances. Pour avoir un décodage rapide, malgré des instructions complexes, le processeur recourt à la technique du pré-décodage, qui prédécode les instructions lors de leur chargement dans le cache d'instruction. Le prédécodage lui-même prend deux cycles, là où une lecture dans le L1 d'instruction en prend 3. les défauts de cache d'instruction sont donc plus longs de deux cycles. Mais l'avantage du prédécodage est que la consommation d'énergie est diminuée. Prenez une instruction exécutée plusieurs fois, dans une boucle. Au lieu de décoder intégralement une instruction à chaque fois qu'on l'exécute, on la prédécode une fois, seul le reste du décodage est fait à chaque exécution. D'où un gain d'énergie assez intéressant. Les caches de micro-opération, qui sont capables d'exécuter une optimisation similaire, n'existaient pas encore à cette époque. ===Le chemin de données de l'Atom=== Les deux décodeurs alimentent une file de micro-opérations de petite taille : 32 µops maximum, 16 par ''thread'' si le ''multithreading'' matériel est activé. La file de micro-opérations a deux ports d'émission, ce qui permet d'émettre au maximum 2 µops par cycle. Les conditions pour cela sont cependant drastiques. Les deux instructions ne doivent pas avoir de dépendances de registres, à quelques exceptions près liées au registre d'état. Le multithreading matériel doit aussi être désactivé. Les deux instructions doivent aller chacun dans un port différent, et cela tient en compte du fait que les deux ports sont reliés à des unités de calcul fort différentes. Le tout est illustré ci-dessous. Les deux ports ont chacun une ALU simple dédiée, capable de faire des additions/soustractions, des opérations bit à bit et des copies entre registres. Mais ils ont aussi des opérations qui leur sont spécifiques. La séparation entre les deux pipelines est assez complexe. Il ne s'agit pas du cas simple avec un pipeline entier et un pipeline flottant séparés. En réalité, il y a deux pipelines, chacun capables de faire des opérations entières et flottantes, mais pas les mêmes opérations. Le premier port permet d’exécuter des opérations entières simples, une addition flottante, des comparaisons/branchements, ou une instruction de calcul d'adresse LEA. Le second port/pipeline est, quant à lui, conçu pour exécuter les instruction ''load-up'' nativement, en une seule micro-opération. Il contient toute la machinerie pour faire les accès mémoire, notamment des unités de calcul d'adresse et un cache L1 de données. A la suite du cache, se trouvent une ALU entière simple, un ''barrel shifter'', et un circuit multiplieur/diviseur. Le circuit multiplieur/diviseur est utilisé à la fois pour les opérations flottantes et entières. [[File:Intel Atom Microarchitecture.png|centre|vignette|upright=2.5|Intel Atom Microarchitecture]] Cette organisation difficile à comprendre est en réalité très efficace, très économe en circuit, tout en gardant une performance intéressante. Les instructions simples, ADD/SUB/bitwise sont supportées dans les deux pipelines. Il faut dire que ce sont des opérations courantes qu'il vaut mieux optimiser au mieux. Le processeur peut donc émettre deux opérations simples et fréquentes en même temps, ce qui augmente les performances. Les opérations plus complexes, à savoir les multiplications/divisions/décalages/rotations/manipulations de bit sont supportées dans un seul pipeline. La raison est qu'il est rare que de telles opérations soient consécutives, et qu'il n'est donc pas utile d'optimiser pour cette situation. Si les deux pipelines devaient supporter ces opérations, cela demanderait de dupliquer les circuits multiplieurs/diviseur, ce qui aurait un cout en circuit important pour un gain en performance assez faible. ===Le système d'exceptions flottantes de l'Atom=== Le processeur étant sans exécution dans le désordre, ses instructions doivent écrire dans les registres dans l'ordre du programme. En conséquence, certaines instructions doivent être retardées, leur émission doit attendre que les conditions soient adéquates. Et cela pose problème avec les opérations flottantes, vu qu'elles prennent pas mal de cycles pour s'exécuter. Imaginez qu'une instruction flottante de 10 cycles soit suivie par une instruction entière. En théorie, on doit retarder l'émission de l'instruction entière de 9 cycles pour éviter tout problèmes. Le cout en performance est donc assez important. En théorie, les instructions entières et flottantes écrivant dans des registres séparés, ce qui fait que l'on pourrait exécuter instructions entières et flottantes dans le désordre. Sauf pour les instructions de copie entre registres entier et flottants, mais laissons-les de côté. Le problème est qu'une instruction flottante peut parfois lever une exception, par exemple en cas de division par zéro, ou pour certains calculs précis. Si une exception est levée, alors l'instruction flottante est annulée, de même que toutes les instructions qui suivent, y compris les opérations entières. Ce n'est pas un problème si le processeur gère nativement les exceptions précises, par exemple avec un tampon de ré-ordonnancement. Mais l'Atom étant un processeur sans exécution dans le désordre, les instructions entières devraient être mises en attente tant qu'une instruction flottante est en cours d'exécution. Heureusement, l'Atom d'Intel a trouvé une parade. La technique, appelée ''Safe Instruction Recognition'' par Intel, est décrite dans le brevet US00525721.6A. L'idée est de tester les opérandes flottantes, pour détecter les combinaisons d'opérandes à problème, dont l'addition/multiplication peut lever une exception. Si des opérandes à problème sont détectées, on stoppe l'émission de nouvelles instructions en parallèle de l'instruction flottante et l'unité d'émission émet des bulles de pipeline tant que l'instruction flottante est en cours. Sinon, l'émission multiple fonctionne. La technique permet ainsi de ne pas écrire dans les registres entiers/flottants dans l'ordre du programme : une instruction entière peut être autorisée à s'exécuter même si elle écrit dans un registre entier avant qu'une instruction flottante délivre son résultat. ==La microarchitecture Netburst du Pentium 4== Dans cette section, nous allons voir l'architecture du processeur Pentium 4, qu'on a volontairement laissée de côté précédemment. Pourquoi un tel saut dans le temps ? Parce que le Pentium est complément à part des autres architectures Intel. Le Pentium 4 a représenté une rupture en termes de microarchitecture, qui a été un échec tellement retentissant que les processeurs suivants sont repartis sur la base du Pentium 3. Il introduisait de nombreuses nouveautés architecturales qui étaient très innovantes. Par exemple, il introduisait le renommage avec un banc de registre physique, qui a été utilisé sur tous les processeurs Intel suivants. Mais la plupart de ces innovations étaient en réalité de fausses bonnes idées, ou du moins des idées difficiles à exploiter. Par exemple, le système de pipeline à ''replay'' n'a été utilisé que sur le Pentium 4 et aucun autre processeur ne l'a implémenté. ===Un focus sur la fréquence d'horloge=== La microarchitecture du Pentium 4 a été déclinée en plusieurs versions, dont les finesses de gravure n'étaient pas les mêmes. La microarchitecture Netburst, utilisée sur le Pentium 4, utilisait un pipeline à 20 étage, augmenté à 32 sur une révision ultérieure. Il a existé quatre révisions de l'architecture : Willamette (180 nm), Northwood (130 nm), Prescott (90 nm) et Cedar Mill (65 nm). Un point important est que le Pentium 4 était prévu pour fonctionner à haute fréquence. Ses 1,5 GHz étaient impressionnants pour l'époque, les autres processeurs tournant à une fréquence proche du GigaHertzs. Pour cela, la solution retenue par Intel a été un pipeline très long, avec beaucoup d'étages. Le Pentium 4 a été décliné en plusieurs versions assez proches, chacune avec sa propre finesse de gravure qui n'ont pas toute le même pipeline. Les micro-architectures ''Willamette'' et ''Northwood'' avaient un pipeline de 20 étages, alors que les autres processeurs de l'époque avaient entre 10 et 15 étages maximum. Les micro-architectures ''Prescott'' et ''Cedar Mill'' étaient une refonte qui a fait grimper le nombre d'étages à 31 ! Du jamais vu, il s'agit d'un record pour un processeur commercial. Un pipeline aussi long permet d'exécuter beaucoup d’instructions en même temps, chacune dans un étage, mais aussi d'atteindre de hautes fréquences facilement. Le problème est qu'un pipeline avec autant d'étages a beaucoup de problèmes. Un point important est que la prédiction de branchement est cruciale. Pour rappel, la pénalité en cas de mauvaise prédiction dépend du nombre d'étages avant que le branchement soit résolu. Et les branchements sont résolus soit en fin de décodage, soit dans l'unité de calcul. C'est à dire au milieu du pipeline, soit en fin de pipeline. La pénalité en cas de mauvaise prédiction de branchement était énorme sur le Pentium 4, elle atteignait facilement 30 cycles Pour compenser, le Pentium 4 avait une prédiction de branchement très performante, pour l'époque. J'insiste sur le pour l'époque. Il utilisait un prédicteur qu'on a déjà abordé dans le chapitre sur la prédiction de branchement, précisément un prédicteur adaptatif à deux niveaux avec un historique global de 16 bits. Il avait aussi un ''Branch Target Buffer'' de 4096 entrées. Mais surtout, il intégrait une sorte de précurseur du cache de micro-opération, appelé le cache de traces, qui est détaillé dans la section suivante. ===Le cache de trace du Pentium 4=== Les décodeurs du Pentium 4 ne font pas décoder les instructions, ils mémorisent le résultat dans un cache de micro-opération un peu particulier, appelé le '''cache de trace'''. Une ligne de cache peut mémoriser 6 micro-opérations, ce qui peu sembler peu mais a été repris sur les micro-architectures suivantes. Mais le cache de trace a une grande différence avec un cache de micro-opération normal. Un cache de micro-opération normal mémorise une instruction par ligne de cache. Une instruction est décodée en plusieurs micro-instructions, qui sont enregistrées dans une ligne de cache. Si l'instruction n'utilise par les 6 micro-opérations disponibles, le reste de la ligne de cache n'est pas utilisé. Mais le Pentium 4 optimise le tout de manière ce à ce que ne soit pas le cas. Sur le Pentium 4, la contrainte du "une instruction par ligne de cache" est abandonnée. Une ligne de cache mémorise 6 micro-opérations consécutives, qui peuvent appartenir à plusieurs instructions. Par exemple, si le décodeur décode 4 instructions consécutives en 6 micro-opérations au total, alors le tout prendra une seule ligne de cache sur le Pentium 4. Et les 4 instructions consécutives n'ont même pas à être consécutives en mémoire : il peut y avoir des branchements pris entre ces instructions ! {|class="wikitable" |+ Cache de trace |- ! Ligne de cache | ADD || SUB || ADD || MOV || MUL || ''shift'' |- ! Ligne de cache | colspan="3" | ADD ''load-up'' || MUL || colspan="2" | Branch if Equal |- ! Ligne de cache | XOR || colspan="4" | POP || SUB |- ! ... | colspan="6" | ... |} Pour expliquer cela plus concrètement, nous allons devoir introduire les concepts de trace et de bloc de base. Un '''bloc de base''' (''basic block'') est une suite d'instructions sans branchement, qui est séparé par deux branchements. Le début d'un bloc de base est la destination d'un branchement, un bloc de base se termine avec un branchement. Une '''trace''' est formée en concaténant plusieurs blocs de base. Pour donner un exemple, regardez le code illustré ci-contre. Il est composé d'un bloc de base A, suivi par un bloc de base B, qui peut faire appel soit au bloc C, soit un bloc D. Un tel code peut donner deux traces : ABC ou ABD. La trace exécutée dépend du résultat du branchement qui choisit entre C et D. Le cache de trace mémorise des traces de 6 micro-opérations consécutives. Les traces sont formées en sortie des décodeurs d'instruction, par de subtiles opérations mélangeant mémorisation, décalage et concaténation. Les circuits qui construisent les traces ne sont pas connus, mais ils doivent certainement être très compliqués. toujours est-il qu'un cache de trace peut mémoriser des traces différentes, même si leur début est le même. Par exemple, prenons deux traces, composées des blocs de base A, B, C et D. La première trace est la trace ABC, la seconde est la trace ABD. Les deux traces auront chacune une ligne de cache dédiée. Une trace est réutilisable quand le premier bloc de base est identique et que les prédictions de branchement restent identiques. Pour vérifier cela, le tag du cache de traces contient l'adresse du premier bloc de base, la position des branchements dans la trace et le résultat des prédictions utilisées pour construire la trace. Le résultat des prédictions de branchement de la trace est stocké sous la forme d'une suite de bits : si la trace contient n branchements, le n-ième bit vaut 1 si ce branchement a été pris, et 0 sinon. Même chose pour la position des branchements dans la trace : le bit numéro n indique si la n-ième instruction de la trace est un branchement : si c'est le cas, il vaut 1, et 0 sinon. Si la trace est réutilisée par la suite, elle est lue depuis le cache de traces. Pour savoir si une trace est réutilisable, l'unité de chargement envoie le ''program counter'' au cache de traces, l'unité de prédiction de branchement fournit le reste des informations. Si on a un succès de cache de traces, et la trace est envoyée directement au décodeur. Sinon, la trace est chargée depuis le cache d'instructions et assemblée. Il faut signaler que le cache de trace avait sa propre unité de prédiction de branchement séparée de l'unité de prédiction de branchement normale. [[File:TraceCache.png|centre|vignette|upright=2|Cache de traces.]] Le cache de traces réduisait la longueur du pipeline en cas de succès de cache de trace. Quand les instructions étaient lues depuis le cache de trace, les étages avant le cache de trace ne sont pas utilisés, tout se passe comme s'ils étaient retirés du pipeline. C'est la même chose avec le cache de micro-opération des processeurs modernes, mais l'idée n'existait pas encore à l'époque. Le cache de trace mémorise des traces décodées, ce qui fait qu'un succès de cache de trace contournait non seulement le cache d'instruction, mais aussi les décodeurs. Le temps d'accès au cache de trace pouvait être assez élevé, même s'il était comparable au temps d'accès du cache d'instruction. Le cache de traces a depuis été remplacé par une alternative bien plus intéressante, le cache de micro-opérations, plus flexible et plus performant. Comparé à un cache de trace, la contrainte "une instruction par ligne de cache" simplifie grandement l'implémentation d'un cache de micro-opération. Ne parlons pas de la détection des succès de cache, qui demande d'utiliser les prédictions de branchement. Mais le vrai problème avec le cache de trace est tout autre. Il arrive souvent qu'une micro-opération soit présente dans plusieurs lignes de cache en raison du processus de construction des traces, chose impossible avec un cache de micro-opération. Et c'est un problème, qui réduit la capacité effective du cache de trace. Alors certes, une ligne de cache est plus remplie que sur un cache de micro-opération, on est certain que les 6 micro-opération par ligne de cache sont remplies. Mais la redondance réduit grandement cet avantage. ===L'exécution dans le désordre et le chemin de données du P4=== Le renommage de registres se fait avec un banc de registres physiques avec une table d'alias. Le Pentium 4 avait une exécution dans le désordre très limitée, basée sur la présence de deux files de micro-opération : une pour les accès mémoire, une autre pour les autres instructions. La seconde file regroupait opérations entières et flottantes, elles n'étaient pas séparées. Avec ces deux files, les instructions mémoire étaient exécutées dans l'ordre du programme, les instructions arithmétiques s'exécutaient aussi dans l'ordre du programme, mais une instruction arithmétique pouvait passer avant une instruction mémoire et inversement. L'avantage est que cela permettait de faire des lectures en avance, c'était une forme limitée de lecture non-bloquantes. Il s'agit bel et bien de deux files d'instructions, pas de fenêtres d'instruction ni de stations de réservation. Le Pentium 4 est le seul processeur commercial qui a utilisé des files de micro-opération séparées, tous les autres utilisent des fenêtres d'instruction : centralisées pour Intel, décentralisées pour AMD (en général). Le Pentium 2 et 3, bien qu'antérieurs, utilisait une station de réservation unique. Cela peut sembler être un retour en arrière, mais les files d'instructions sont bien plus larges : de 42 micro-opérations pour le Pentium 3, on passe à 120 micro-opérations pour le Pentium 4. Et vu la longueur du pipeline, qui fait qu'il y a plus d'instructions en vol, c'était une nécessité. Mais cela n'aurait pas été possible en utilisant des stations de réservation, pour des raisons de consommation électrique et/ou de budget en transistors, ce qui fait que passer à une file de micro-opération été la solution retenue. Pour les accès mémoire, le Pentium 4 utilisait donc une file de µops mémoire unique, couplée à une file d'écriture (non-représentée sur les schémas qui suivent). La file d'écriture du Pentium 4 était de 24 écritures maximum et gérait le ''Store-to-load forwarding''. Le processeur pouvait émettre une lecture et une écriture à chaque cycle. Il y avait un port d'émission pour les lectures et un autre pour les écritures, tous deux ayant chacun leur propre unité de calcul d'adresse. Le processeur contenait 3 ALU entières, 2 unités de calcul d'adresse et une FPU. La FPU était complétée par une unité pour faire des copies entre registres flottants, des opérations MOV. Pour les AGU, il y en avait une dédiée aux lectures, une autre pour les écritures. Le tout était relié aux ports d'émissions comme suit : [[File:Ports d'émission du Pentium 4.png|centre|vignette|upright=2.5|Ports d'émission du Pentium 4]] Le processeur utilisait deux réseaux de contournement séparés : un pour les opérations flottantes, un pour les opérations entières. Le réseau de contournement pour les opérations entières est aussi relié aux unités de calcul d'adresse. Jusque là, rien de surprenant, le chemin de données du processeur est assez classique. [[File:Architettura Pentium 4.png|centre|vignette|upright=3|Microarchitecture du Pentium 4.]] ===Les unités de calcul entières du Pentium 4=== Sur le Pentium 4, les ALU entières étaient cadencées à une fréquence double de celle du processeur. Les ALU entières pouvaient exécuter deux micro-opérations par cycle, ce qui fait que les ports d'émissions reliés aux ALU devaient eux aussi fonctionner à double fréquence. Pour faire la différence entre les deux fréquences, nous parlerons de fréquence/cycle processeur et de fréquence/cycle de l'ALU. Précisons que seules les ALU entières étaient à double fréquence, pas le multiplieur, pas le ''barrel shifter''. Pour simplifier, nous allons parler d'additionneur plutôt que de l'ALU entière, ce qui sera plus proche de la réalité. Et l'implémentation de l'additionneur du Pentium 4 était très innovante. L'additionneur pouvait exécuter deux additions par cycle, même si les deux additions ont une dépendance. Mais n'allez pas croire que l'implémentation était intuitive, avec un additionneur 32 bit basique très rapide. Non seulement l'additionneur fonctionnait à double fréquence, mais il était aussi pipeliné, avec un système de contournement interne ! Les additionneurs étaient pipelinées, d'une manière très simple. Une addition 32 bits était découpée en trois étapes : deux additions de 16 bits, une dernière étape pour mettre à jour le registre d'état. Pour cela, chaque additionneur était composé de deux additionneur 16 bits chacune, placées l'une après l'autre, avec un registre de pipeline entre les deux. L'additionneur prenait deux cycles d'horloge pour faire son travail : le premier cycle calculait les 16 bits de poids faible, le second calculait les 16 bits de poids fort lors du second cycle. Le tout est appelé '''addition étagée''' (''staggered add'') dans la documentation Intel. Une addition se fait donc en deux étapes, sauf que c'est compensé par le fait que l'additionneur fonctionnait à une fréquence double de celle du processeur ! Le résultat de ce fonctionnement franchement bizarre, est que les 16 bits de poids faible étaient calculés en une moitié de cycle processeur, alors que l'opération complète prenait un cycle. Deux additions consécutives s'exécutaient donc en 1 cycle et demi, alors qu'on aurait cru au premier abord que cela prendrait seulement un cycle. Si on fait les calculs, on s'apercoit que le rythme de croisière est cependant proche de 2 additions par cycle, bien qu'inférieur. 3 additions consécutives se font en deux cycles, 5 additions en 3 cycles, 7 en 4 cycles, etc. Et le Pentium 4 ajoutait un système de contournement interne à l'ALU. En clair, si une addition utilise le résultat de l'addition précédente, les deux peuvent s'exécuter en un cycle d'horloge et demi. Les 16 bits de poids faible de la première addition sont disponibles après un cycle ALU, ce qui permet de démarrer le calcul de la seconde addition au cycle suivant. ===Le ''replay pipeline''=== Le processeur est un processeur triple émission : il peut charger et décoder 3 µops par cycle, le ROB peut terminer 3 µops par cycle, etc. Pourtant, les ports d'émission peuvent émettre 6 instructions par cycle : 4 ports, dont deux à double fréquence. Une telle différence s'explique par l'usage d'un ''replay pipeline'', dont nous avons déjà parlé dans ce cours. Pour rappel, le Pentium 4 suppose que les lectures font tous un succès de cache L1. Si une opération arithmétique utilise la donnée lue comme opérande, le processeur l'émet immédiatement, l'opérande sera disponible une fois l'opération en entrée de l'ALU entière. Mais s'il s'est trompé, le processeur ré-exécute l'instruction après un temps d'attente de quelques cycles, pour se caler sur la latence du cache L2. Et si il y a un défaut de cache L2, l’instruction attend encore. Cela demande de ré-exécuter des instructions émises à tord, ce qui fait que le processeur doit avoir la capacité d'exécution pour. Ce pourquoi le processeur peut émettre 6 µops dans les unités de calcul : 3 µops normales et 3 µops ré-exécutées. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Les processeurs superscalaires | prevText=Les processeurs superscalaires | next=Les processeurs VLIW et EPIC | nextText=Les processeurs VLIW et EPIC }} </noinclude> ddd859416z5npflvcb5f2pdi5tpde4i 772805 772757 2026-09-22T07:02:20Z DavidL 1746 /* Un étude des microarchitectures superscalaires x86 d'AMD */ 772805 wikitext text/x-wiki Dans ce chapitre, nous allons étudier des exemples de processeurs x86, ceux présents dans nos PC. Nous n'allons pas voir les anciens processeurs comme le 286, le 386 ou le 486. Nous allons commencer avec le Pentium 1, et les processeurs commerciaux qui ont suivis. Tous les processeurs que nous allons voir dans ce chapitre sont des processeurs superscalaires. De fait, ce n'est pas pour rien si ce chapitre se situe après le chapitre sur les processeurs superscalaires. Par contre, nous allons voir que certains n'ont pas d'exécution dans le désordre. La raison est que l'exécution dans le désordre est arrivé après la superscalarité. ==Généralités sur les CPU x86 superscalaires== Avant de voir chaque processeur indépendamment des autres, nous allons devoir aborder quelques généralités. Nous allons d'abord voir que le jeu d'instruction x86 pose quelques problèmes pour la superscalarité. Et ces problèmes posent des contraintes assez fortes, avec lesquelles les concepteurs de processeurs dovient faire avec. Nous poursuivrons ensuite par un historique des processeurs Intel et AMD, histoire de donner un peu de contexte aux processeurs que nous allons étudier. ===Le jeu d'instruction x86 pose des problèmes pour la superscalarité=== Une difficulté de l'architecture x86 est qu'il s'agit d'une architecture CISC, avec tous les défauts que ça implique. Un jeu d'instruction CISC a en effet de nombreuses propriétés qui collent mal avec l'émission multiple, avec la '''superscalarité'''. Il y en a plusieurs, certaines impactent le chargement des instructions, d'autres leur décodage, d'autres l'exécution, etc. Premièrement, les instructions sont de longueur variable, entre 1 et 15 octets, ce qui complique leur chargement et leur décodage. En pratique, les processeurs chargent un bloc de 32 à 64 octets, et découpent celui-ci en plusieurs instructions. La conséquence est que l'usage d'instructions trop longues peut poser problème. Imaginez qu'un processeur charge un bloc de 16 octets et que celui-ci ne contienne qu'une seule instruction : on ne profite pas de la superscalarité. Deuxièmement, une partie des instructions est microcodée, faute de mieux. Et cela pose de sérieux challenges pour l'implémentation des décodeurs. Dupliquer le microcode demanderait trop de transistors, ce qui fait que ce n'est pas fait. À la place, il n'y a qu'un seul microcode, ce qui fait que l'on ne peut pas décoder plusieurs instructions microcodées en même temps. Il est cependant possible de profiter de la superscalarité, en décodant une instruction microcodée en parallèle d'autres instructions non-microcodées. Et heureusement, ce cas est de loin le plus fréquent, il est rare que plusieurs instructions microcodées se suivent. Troisièmement, la présence d'instructions ''load-up'', qui lisent un opérande en mémoire, peut poser problème, mais est aussi source d'optimisations assez intéressantes. En théorie, une instruction ''load-op'' est décodée en deux micro-opération : une pour lire d'opérande en RAM, l'autre pour faire l'opération arithmétique. Sauf que les processeurs x86 modernes optimisent la gestion des instructions ''load-up''. Par exemple, les premiers processeurs Atom géraient des micro-opérations de type ''load-up'', directement dans le chemin de données ! D'autres processeurs utilisent la technique de la '''micro-fusion''' pour retarder le décodage réel des instructions ''load-up'' assez loin dans le pipeline. Avec eux, une instruction ''load-op'' est décodée en une seule "macro-opération", qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. L'avantage est qu'une macro-opération ne prend qu'une seule entrée dans le tampon de ré-ordonnancement, la fenêtre d'instruction, la file de micro-opération, et les autres structures similaires. ===Un petit historique des processeurs x86 superscalaires=== Nous allons voir les processeurs Intel à part des processeurs AMD. La raison à cela est que les architectures Intel et AMD ont progressivement évolué, chacune se basant sur la précédente et l'améliorant. Il n'y a pas eu de cassure entre microarchitectures AMD, qui sont chacune la suite de la précédente. Il est donc préférable de voir les architectures AMD dans l'ordre chronologique. Par contre, Intel a eu une gigantesque cassure, avec le processeur Pentium 4. Son architecture se démarquait fortement du Pentium 3, mais elle n'a pas convaincu et a été abandonnée avec les processeurs suivants. Ce qui fait nous verrons l'architecture du Pentium 4 à part. Le Pentium 1 et 2 utilisaient la même architecture, qu'on détaillera dans ce qui suit. Les seules différences importantes étaient la fréquence, le cache, et quelques détails dans le genre. Le Pentium 3 était une nouvelle microarchitecture qui ajoutait l'exécution dans le désordre. Un an et demi plus tard, le Pentium 4 est sorti et a été un échec. Ses performances étaient peu convaincantes face au Pentium 3, et sa consommation énergétique était très importante. La conséquence est que le Pentium 4 et le Pentium 3 ont survécu pendant un long moment, beaucoup de monde préférait acheter un Pentium 3. Intel a alors amélioré les microarchitectures du Pentium 3 et du 4, indépendamment, pendant environ 7 ans. La microarchitecture du Pentium 3 a subit plusieurs micro-évolutions, chacune avec une finesse de gravure différente, afin de satisfaire les consomateurs. Les premeirs Pentium 3 avaient une finesse de gravure de 250 nm, elle a chuté à 65 sur les derniers modèles. L'architecture du Pentium 4 a fait la même chose, pour tenter de corriger ses problèmes de performance et de consommation d'énergie. Les premiers Pentium 4 avaient uen finesse de gravure de 180 nm, elle a elle aussi chutée à 65 sur les derniers modèles. Après l'échec du Pentium 4, les ingénieurs d'Intel ont repris l'architecture P6 et l'ont améliorée fortement, pour donner l'architecture Core. Les micro-processeurs suivants ont fait évoluer cette architecture progressivement, au point où elle ne ressemble plus à l'originale. L'architecture Core a laissé la place à l'architecture Nehalem, puis Sandy Bridge, puis Haswell, puis Skylake, puis Ice Lake, et Golden Cove. Il s'agit de la lignée principale, partant du Pentium 3 et continuant jusqu'à nos jours. Ces microarchitectures ont suivi un motif assez simple, appelé modèle '''tick-tock'''. Chaque microarchitecture était déclinée en deux versions, la seconde ayant une finesse de gravure réduite. En parallèle, Intel a travaillé sur des processeurs basse performance et basse consommation, avec une microarchitecture très différente. Les processeurs Atom de microarchitecture Bonnel, pour être ensuite remplacés par les microarchitectures Silvermont, puis Goldmont et Gracemont. Ces microarchitectures ont évolué en parallèle de la lignée principale, il s'agit d'une lignée secondaire. Le tout est résumé dans ce schéma ci-dessous. [[File:IntelProcessorRoadmap-4v.svg|centre|vignette|upright=2.5|Roadmap des processeurs Intel, qui servira de structure pour la suite du chapitre]] De nos jours, les processeurs Intel utilisent les deux types de micro-architectures en même temps. Les CPU Intel modernes disposent de deux types de coeurs : les coeurs P et le coeurs E. Leur nom signifie "Performance" et "Efficient", qui trahissent leur but. Les coeurs P utilisent la microarchitecture de la lignée principale, qui est conçue pour la performance. Les coeurs E, quant à eux, utilisent les micro-architectures basse consommation de la lignée secondaire. ==Les processeurs x86 d'Intel, la lignée principale== Pour commencer, nous allons voir les processeurs Intel. N'y voyez pas du favoritisme derrière ce choix, la justification est toute autre. Si je commence par Intel, c'est pour commencer avec les Pentium 1 et 2, qui sont plus simples que tous les autres. Il s'agit en effet de processeurs superscalaires, mais sans exécution dans le désordre. L’absence d'exécution dans le désordre les rend bien plus simples à étudier que les autres, ce qui en fait un bon point de départ pour ce chapitre. Le successeur du Pentium 2 a intégré l'exécution dans le désordre, ce qui fait que les Pentium 1 et 2 sont les seuls processeurs superscalaires ''in-order''. Pour la concurrence, AMD n'a pas produit de processeurs ''in-order'', tous les processeurs produits par AMD intègrent l'exécution dans le désordre. A la rigueur, il y a bien les processeurs Atom de la lignée secondaire d'Intel. Cependant, même s'ils sont bien des CPU ''in-order'', leur architecture est assez compliquée. En comparaison, le Pentium est une vieille architecture, qui se débrouillait avec peu de transistors et était donc bien plus simple que celle de l'Atom. ===Le Pentium 1/MMX et les pipelines U/V=== Le processeur Pentium d'Intel avait un pipeline de 5 étages : un étage de chargement/prédiction de branchement, deux étages de décodage, un étage d'exécution et un dernier étage pour l'écriture dans les registres. Le Pentium 1 était un processeur double émission, intégrant deux pipelines nommés U et V. Chose importante, les deux pipelines n'étaient pas identiques. Le pipeline U pouvait exécuter toutes les instructions, mais le pipeline V était beaucoup plus limité. Par exemple, seul le pipeline U peut faire des calculs flottants, le pipeline V ne fait que des calculs entiers et des branchements. Les deux pipelines disposaient d'une unité de calcul entière, identique dans les deux pipelines. Mais le pipeline U incorporait un circuit multiplieur/diviseur et d'un ''barrel shifter''. L'unité flottante était sur le port d'émission du pipeline U, idem pour l'unité de calcul vectoriel MMX sur le Pentium MMX. Les deux pipelines avaient chacun une unité de calcul d'adresse, mais ils n'étaient pas identiques : celle du pipeline V ne gérait que l’instruction LEA, celle du pipeline U gérait tous les calculs d'adresse. {|class="wikitable" |- ! Pipeline U ! Pipeline V |- | ALU entière | ALU entière |- | Multiplieur/diviseur | |- | ''Barrel Shifter'' | |- | AGU complexe | AGU simple (opération LEA) |- | FPU | |- | Unité SIMD | |} Les deux pipelines géraient les opérations bit à bit, les additions, les soustractions et les comparaisons. Les autres instructions ne sont exécutables que dans le pipeline U. Pour être plus précis, les deux pipelines supportaient les instructions suivantes, ce qui fait qu'on pouvait en faire deux en même temps : * Les instructions arithmétiques INC, DEC, ADD, SUB ; * l'instruction de comparaison CMP ; * les instructions bit à bit AND, OR, XOR ; * l'instruction de calcul d'adresse LEA ; * l'instruction MOV (dépend du mode d'adressage) ; * les instructions de gestion de la pile PUSH et POP (dépend du mode d'adressage) ; * l'instruction NOP, qui ne fait rien. Il faut noter qu'il y a cependant quelques restrictions, beaucoup de paires d'instructions sont interdites. La plupart interdisent au pipeline V de faire quoique ce soit quand une opération particulière est émise dans le pipeline U. Par exemple, si le pipeline U exécute une multiplication ou une division, le processeur ne peut pas exécuter une opération dans le pipeline V. Et c'est pareil avec les branchements : si un branchement est émis dans le pipeline U, l'instruction suivant le branchement n'est pas émise dans le pipeline V, pour éliminer les dépendances de contrôle. De même, si le pipeline U exécute une opération flottante, le pipeline V ne pourra rien exécuter. La seule exception est l'instruction FCXH, qui échange deux registres flottants. [[File:Intel Pentium arch.svg|centre|vignette|upright=2.5|Microarchitecture de l'Intel Pentium MMX. On voit que certaines unités de calcul sont dupliquées.]] Un choix assez intéressant a été fait pour le cache de données. Nous avions vu dans le chapitre sur les CPU superscalaires que la superscalarité a un impact sur l'unité mémoire. Il y a alors deux implémentations. La première ne fait rien, l'unité mémoire ne change pas, et le processeur ne peut pas faire deux accès mémoire simultanés. La seconde duplique l'unité mémoire et les ports de lecture/écriture du cache, ce qui autorise des accès mémoire simultanés. Le Pentium 1 utilise une solution intermédiaire. Les ingénieurs d'Intel étaient partis à la base sur un cache totalement double port, pour obtenir des performances maximales. Mais diverses simulations et observations les ont fait changer d'avis. Les simulations ont montré qu'il est "rare" que les deux pipelines aient besoin de lire/écrire dans le cache en même temps. Et ils ont optimisé le cache de donnée pour en tenir compte. Le cache de données est partiellement multiport : simple port sur certains aspects, double port sur d'autres. Le cache est un cache splité, à savoir que les données et les ''tags'' sont séparés dans des mémoires séparées. La mémoire pour les ''tags'' est multiport, ce qui permet d'interroger les ''tags'' du cache deux fois par cycle. Un port est relié au pipeline u, un autre au pipeline V. Mais pour les données, le cache n'a qu'un seul port pour lire/écrire des données. Impossible donc de lire deux données en même temps, pour alimenter les deux pipelines. Il utilise cependant 8 banques permet d'accélérer des accès mémoire proches dans le temps, mais dans des cycles d'horloge différents. Entre les deux mémoires, il y a un circuit qui détecte les conflits, à savoir les situations où les deux pipelines accèdent en même temps au cache. S'ils veulent lire/écrire une donnée dans le même cycle, ce qui est impossible avec un seul port, le pipeline U a la priorité et le pipeline V attend le cycle suivant. Le circuit détecte aussi les conflits de banque, à savoir quand un pipeline accède à une banque en cours d'accès par l'autre pipeline (on rappelle qu'un accès au cache prend plusieurs cycles). Le circuit détecte aussi certaines dépendances mémoires, à savoir des accès consécutifs à la même adresse. : La TLB du processeur est aussi totalement double port. ===La microarchitecture P6 du Pentium 2/3=== Le Pentium 3 utilisait la '''microarchitecture P6''', qui a été dérivée dans de nombreuses variantes, dont les finesses de gravure n'étaient pas les mêmes. Il introduit une exécution dans le désordre simple, avec une fenêtre d'instruction centralisée, avec renommage dans le désordre dans le ROB (tampon de ré-ordonnancement), commandé par une table d'alias. C'était un processeur triple émission, soit une instruction de plus que la double émission du Pentium 1. Le pipeline passe de 5 étage sur le Pentium à 14 - 12 étages, dont le détail est le suivant : * Prédiction de branchement, deux cycles ; * Chargement des instructions, trois cycles ; * Décodage de l'instruction, deux cycles ; * Renommage de registre, un cycle ; * Copie des opérandes dans le tampon de ré-ordonnancement (lié au renommage de registre dans le ROB) ; * Dispath dans ou depuis la station de réservation. * Exécution de l'instruction ; * Écriture du résultat dans le ROB ; * Écriture dans le banc de registre physique. Les instructions sont chargées par blocs de 16 octets, avec un système de fusion de blocs pour gérer les instructions à cheval sur deux blocs. Lors d'un branchement, deux blocs doivent être chargés si l'instruction de destination n'est pas alignée sur 16 octets et cela cause un délai de un cycle d'horloge. Le décodage des instructions x86 était géré par plusieurs décodeurs. Il y avait trois décodeurs : deux décodeurs simples, et un décodeur complexe. Les décodeurs simples décodaient les instructions les plus fréquentes, mais aussi les plus simples, qui étaient décodées en une seule micro-opération. Les instructions CISC complexes étaient gérées uniquement par le décodeur complexe, basé sur un microcode, qui pouvait fournir jusqu'à 4 micro-opérations par cycle. Le tout est résumé avec la règle 4-1-1. La toute première instruction chargée depuis la file d'instruction va dans le premier décodeur simple. Si jamais le décodeur ne peut pas décoder l'instruction, l'instruction est redirigée dans un autre décodeur, avec un délai d'un cycle d'horloge. Les stations de réservations étaient regroupées dans une structure centralisée, en sortie de l'unité de renommage. Elles avaient 5 ports d'émission, qui étaient sous-utilisés en pratique. Niveau ALU, on trouve deux ALUs entières, une flottante, une unité pour les instructions SSE et autres, et trois unités pour les accès mémoire (regroupées en une seule unité dans le schéma ci-dessous). Les unités mémoire regroupent une unité de calcul d'adresse pour les lectures, une autre pour les écritures, et une unité pour la gestion des données à écrire. Les unités de calcul d'adresse sont des additionneurs à 4 opérandes, complétement différents des ALU entières. Les ALU entières sont deux unités asymétriques : une ALU simple, et une ALU complexe incorporant un multiplieur. Les deux peuvent exécuter des opérations d'addition, soustraction, comparaison, etc. [[File:P6 func diag.png|centre|vignette|upright=2|P6 func diag]] Les premiers Pentium 3 n'avaient pas de cache L2 dans le processeur, celui-ci était sur la carte mère. Mais il a été intégré dans le processeur sur la seconde version du Pentium 3, la version Coppermine. Le Pentium 3 a servi de base aux microarchitectures d'Intel qui ont suivi. Les changements à chaque nouvelle génération sont assez mineurs : la prédiction de branchement est améliorée, la taille des stations de réservation et du ROB augmente, idem avec les autres structures liées à l'exécution dans le désordre. Les processeurs Intel ont conservé une fenêtre d'instruction centralisée, alors qu'AMD utilise une autre méthode, comme nous allons le voir dans ce qui suit. Les seuls changements notables sont est le passage à un renommage dans le ROB à un renommage à banc de registre physique, ainsi que l'introduction du cache de micro-opération. Et ce sont des modifications qu'AMD a aussi faites, celle-ci étant clairement une bonne idée pour toutes les micro-architectures avec un budget en transistor suffisant. Il est intéressant de garder cela en tête, car une bonne partie des améliorations de chaque micro-architecture proviendra de là. ===La microarchitecture Core=== La '''microarchitecture Core''' fait suite au Pentium 4, mais reprend en fait beaucoup d’éléments du Pentium 2 et 3. Elle utilise la station de réservation unique avec renommage dans le ROB, provenant du Pentium 2/3. Elle supporte aussi les optimisations des opérations ''load-up'', avec notamment un support des macro-opérations mentionnées plus haut. Les améliorations sont assez diverses, mais aussi assez mineures. * Le processeur incorpore un cache L2, en plus des caches L1 déjà présents auparavant. * La prédiction de branchement a été améliorée avec notamment l'ajout d'une ''Fetch Input Queue''. * L'architecture Core passe à la quadruple émission, soit une instruction de plus que sur le Pentium 2 et 3. Pour cela, un quatrième décodeur est ajouté, il s'agit d'un décodeur simple qui ne fournit qu'une seule micro-opération en sortie. * Un ''stack engine'' et un ''Loop Stream Detector'' ont été ajoutés, ainsi que le support de la macro-fusion qui fusionne une instruction de test et le branchement qui suit en une seule micro-opération. * Les techniques de désambiguïsation mémoire sont implémentées sur cette micro-architecture. Il y a quelques modifications au niveau de l'unité de chargement. La file d'instruction a toujours ce système de fusion de blocs, sauf que les branchements ne causent plus de délai d'un cycle lors du chargement. La file d'instruction est suivie par un circuit de prédécodage qui détermine la taille des instructions et leurs frontières, avant de mémoriser le tout dans une file de 40 instructions. La station de réservation dispose de 6 ports d'émission, mais on devrait plutôt dire 5. Sur les 5, il y en a un pour les lectures, un pour les écritures. Les deux sont reliées à une ''Load/Store Queue'', appelée ''Memory Ordering Buffer''. Elle est elle-même reliée au cache de données par deux ports : un port de lecture et un port d'écriture. Les trois ports d'émission restants sont connectés aux unités de calcul. Les trois ports récents sont chacun reliés à une ALU entière. L'additionneur flottant est connecté au port 1, alors que le multiplieur/diviseur flottante est connecté au port 0. Le fait de mettre les deux sur des ports différents permet d'émettre une addition et une multiplication flottant simultanément. Le multiplieur entier est relié au second port d'émission, celui sur lequel se trouve l'additionneur flottant. Le résultat que le processeur peut émettre un mix d'opérations flottantes et entière assez varié. [[Image:Intel Core2 arch.svg|centre|vignette|upright=2|Intel Core microarchitecture]] ===Les microarchitectures Sandy Bridge and Ivy Bridge=== Les micro-architectures suivant la micro-architecture Core ont introduit quelques grandes modifications : le passage à un renommage à banc de registre physique, l'ajout d'un cache de micro-opérations (et d'un ''Loop Stream Detector''). L'ajout du cache de micro-opérations est un gros changement, particulièrement avec le jeu d’instruction x86. Le décodage des instructions est lent, couteux en énergie. Mais avec l'introduction du cache de micro-opération, la majorité des micro-opérations est non pas décodée, mais lue depuis le cache de micro-opérations. Les décodeurs décodent les instructions pas encore exécutées, mais les exécutions suivantes sont lues depuis le cache de micro-opérations. Et vu la grande présence de boucles, le cache de micro-opérations est l'alimentation principale du pipeline. Les décodeurs servent surtout à alimenter le cache de micro-opérations, parfois décoder quelques instructions isolées exécutées de-dehors de boucles, pas plus. Concrètement, ils servent pour 10 à 20% des micro-opérations exécutées. Intel a d'ailleurs reflété ce fait dans sa terminologie. Intel distingue deux voies de chargement : le ''legacy pipeline'' et le cache de micro-opérations. L'unité de chargement et les décodeurs sont regroupés dans la voie du ''legacy pipeline''. Le cache de micro-opérations est complété avec un ''Loop Stream Detector'', placé après le cache en question. Les décodeurs et le cache de micro-opérations alimentent une file de micro-opérations, située juste avant l'étage de renommage de registres. La file de micro-opérations sert en quelque sorte de tampon entre l'étage de "décodage" et celui de renommage. Le ''Loop Stream Detector'' utilise cette file de micro-opérations comme d'un cache lorsqu'une boucle est détectée. Les micro-opérations de la boucle sont lue depuis la file de micro-opérations, pour être envoyée au renommeur de registres. L'avantage est que le cache de micro-opérations et/ou les décodeurs sont mis en pause et clock-gatés lorsqu'une boucle s'exécute, ce qui réduit la consommation du processeur. Le ''Loop Stream Detector'' et le cache de micro-opération ont globalement le même effet : désactiver tout ce qui est avant, le ''Loop Stream Detector'' appliquant cette méthode au cache de micro-opération lui-même.. Voyons maintenant quelles sont les micro-architectures qui implémentent ces optimisations. Les microarchitectures '''Sandy Bridge''' and '''Ivy Bridge''' sont similaires à l'architecture Core, si ce n'est pour le passage à un renommage à banc de registre physique, et l'ajout d'un cache de micro-opérations. Le nombre de ports d'émission passe à 7, avec 4 pour les instructions arithmétiques (flottantes comme entière), 2 pour les lectures, et un pour les écritures (en fait deux, avec un pour le calcul d'adresse, l'autre pour la donnée à écrire). Pour le reste, rien ne change si ce n'est la prédiction de branchement Les architectures '''Haswell''' et '''Broadwell''' ont ajouté quelques unités de calcul, élargit la sortie du cache de micro-opérations. Un port d'émission pour opération entières a été ajouté, de même qu'un port pour les accès mémoire. Le processeur passe donc à 8 ports d'émission, ce qui permet d'émettre jusqu'à 8 micro-opérations, à condition que le cache de micro-opération suive. Pour le reste, le processeur est similaire aux architectures précédentes, si ce n'est que certaines structures grossissent. L'architecture '''Skylake''' réorganise les unités de calcul et les ports d'émission pour gagner en efficacité. Pour le reste, les améliorations sont mineures. A la rigueur, l'unité de renommage de registre ajoute des optimisations comme l'élimination des MOV, les idiomes liés aux opérations avec zéro, etc. ===Les microarchitectures récentes d'Intel=== Les architectures '''Ice Lake''' et '''Tiger Lake''' passent de quadruple émission à la pentuple émission. Par contre, le processeur utilise toujours 4 décodeurs. Mais les micro-opérations étant émises depuis le cache de micro-opérations, ce n'est pas un problème pour la pentuple émission. Le processeur peut parfaitement émettre 5 micro-opérations en même temps, si elles sont lues depuis le cache de micro-opérations. Là encore, on voit à quel point le cache de micro-opération découple ce qu'il y avant de ce qu'il y a après. La microarchitecture '''Golden Cove''' altère les décodeurs et l'unité de chargement. Sur toutes les générations précédentes, on reste sur une unité de chargement qui charge 16 octets à la fois et il y a toujours 4 décodeurs identiques aux générations précédentes. Golden Cove passe à 6 décodeurs simples, et double la taille du chargement qui passe à 32 octets. Une telle stagnation sur les unités de chargement et de décodage s'explique encore une fois par la présence du cache de micro-opération fait que ce n'est pas trop un problème. Tout ce qui précède le cache de micro-opérations n'a pas de raison d'évoluer, car ce cache est très puissant. Niveau unités de calcul, le CPU a pas moins de 5 ALU entières, deux ''barrel shifters'', un multiplieur et deux unités de branchements. Le tout est répartit sur 5 ports d'émission. Pour les unités mémoire, il y a trois unités LOAD pour les lectures et deux unités STORE pour les écritures. Il n'y a plus d'unité flottante proprement dite, mais une unité SIMD qui est capable de faire plusieurs calculs flottants, qu'on ne détaillera pas ici, car nous n'avons pas encore vu les techniques de SIMD. Notons qu'il y a trois fenêtres d'instruction séparées pour : les instructions entières/flottantes, les lectures, les écritures. Le processeur peut décoder 6 instructions par cycle, et en émettre 6 à destination des fenêtres d'instructions. Les fenêtres d'instructions peuvent émettre 5 instructions entières, trois lectures et deux écritures en même temps. Le ROB accepte de ''commit'' 8 µops par cycle. [[File:Golden Cove.png|centre|vignette|upright=3|Golden Cove]] Il s'agit donc de processeurs superscalaires très larges, ce qui est la norme de nos jours et le restera pendant longtemps. Et il faut noter que même les cœurs basse performance sont dans ce cas. Pour rappel,n les CPU Intel modernes regroupent deux types de cœurs : les cœurs P et les cœurs E. Les cœurs P sont des cœurs haute performance, basés sur les microarchitectures Cove, optimisées pour la performance. Les cœurs E, quant à eux, utilisent une microarchitecture différente, conçue pour économiser de l'énergie et avoir une consommation réduite, au prix de performances réduites. Pourtant, même les coeurs E utilisent une superscalarité large ! Par exemple, voici ce que donne la microarchitecture Gracemont. Elle peut décoder deux paquets de 3 instructions chaque. Elle envoie ensuite 5 µops à l'unité d'émission, qui sont distribuées dans deux pipelines : un pipeline flottant/SIM et un pipeline entier. Et le pipeline entier peut émettre 4 µops entières, deux lectures, deux écritures et deux branchements ! Pire que ça, le ''barrel shifter'' et le multiplieur/diviseur sont dupliqués en deux exemplaires ! [[File:Gracemont.png|centre|vignette|upright=3|Gracemont]] La différence entre les microarchitectures P et E tient dans le système d'exécution dans le désordre. Le ROB, les fenêtres d'instructions, les différentes files de µops et caches : tout est plus petit sur les coeurs E. De plus, les coeurs E utilisent des files d'instructions pour les opérations mémoire et flottantes, au lieu de fenêtres d'instruction. Le schéma ci-dessous illustre ce qu'il en est sur l'architecture Crestmont, pour coeur sE. On voit que les files pour µops flottantes et mémoire sont de type ''non-scheduling'', ce qui veut dire que ce sont de simples files d'instruction. Les performances mémoire et flottantes sont donc un peu réduites. [[File:Crestmont.png|centre|vignette|upright=3|Crestmont]] ==Une étude des micro-architectures superscalaires x86 d'AMD== Les architectures Intel ont évolué progressivement, sans grandes cassure. Il y a une continuité presque ininterrompue entre l'architecture du Pentium 2 et les architectures modernes. Intel a fait des améliorations mineures à chaque nouvelle micro-architecture, si on omet le passage à un renommage à banc de registre physique et l'ajout du cache de micro-opération. A l'opposé, les architectures AMD ont eu de nombreuses cassures dans la continuité où AMD a revu sa copie de fond en comble. Étudier ces architectures demande de voir trois choses séparément : le ''front-end'' qui regroupe l'unité de chargement et les décodeurs, le ''back-end'' qui gère l'exécution dans le désordre et les unités de calcul, et le sous-système mémoire avec les caches et la ''Load Store Queue''. Leur étude sera plus ou moins séparée dans ce qui suit, pour chaque classe d'architecture. ===La première génération de CPU AMD : les architectures K5, K6, K7, K8 et K10=== La première génération de processeurs AMD est celle des architectures K5, K6, K7, K8 et K10. Il n'y a pas de K9, qui a été abandonné en cours de développement. Les processeurs K5 et K6 portent ce nom au niveau commercial. Par contre, les processeurs d'architecture K7 sont aussi connus sous le nom d''''AMD Athlon''', les AMD K8 sont connus sous le nom d''''AMD Athlon 64''', et les architecture K10 sont appelées les '''AMD Phenom'''. Comme le nom l'indique, l'architecture K8 a introduit le 64 bits chez les processeurs AMD. Elles ont une architecture assez similaire pour ce qui est du chargement et des caches. Toutes disposent d'au minimum un cache L1 d'instruction et d'un cache L1 de données. Le K5 n'avait que ces caches, mais un cache L2 a été ajouté avec le K7, puis un L3 avec le K10. L'AMD K5 avait une TLB unique, mais les processeurs suivants avaient une TLB pour le L1 d'instruction et une autre pour le L1 de données. Idem pour le cache L2, avec deux TLB : une pour les données, une pour les instructions. Les caches L1/L2 sont de type exclusifs, à savoir que les données dans le L1 ne sont pas recopiées dans le L2. Le cache L2 est précisément un cache de victime, qui mémorise les données/instructions, évincées des caches L1 lors du remplacement des lignes de cache. L'introduction du cache L2 a entrainé l'ajout de deux TLB de second niveau : une L2 TLB pour les données et une autre pour les instructions. Les architectures K8 et K10 ont ajouté un cache L3, avec un accès indirect à travers l'interface avec le bus. : L'AMD K7 originel, aussi appelée Athlon classique, n'avait pas de cache L2, mais celui-ci était placé sur la carte mère et fonctionnait à une fréquence moitié moindre de celle du CPU. L'Athlon Thunderbird, puis l'Athlon XP, ont intégré le cache L2 dans le processeur. {|class="wikitable" |- ! Architecture AMD ! colspan="5" | Caches |- | rowspan="2" | K5 | L1 instruction || L1 données || colspan="3" | |- | colspan="2" | TLB unique || colspan="3" | |- | colspan="4" | |- | rowspan="2" | K6 | L1 instruction || L1 données || colspan="3" | L2 unifié |- | TLB L1 instruction || TLB L1 données || colspan="3" | |- | colspan="6" | |- | rowspan="2" | K7, K8 | L1 instruction || L1 données || colspan="2" | L2 unifié || |- | TLB L1 instruction || TLB L1 données || TLB L2 instruction || TLB L2 données || |- | colspan="6" | |- | rowspan="2" | K10 | L1 instruction || L1 données || colspan="2" | L2 unifié || L3 |- | TLB L1 instruction || TLB L1 données || TLB L2 instruction || TLB L2 données || |} Fait important, les architectures K5 à K10 utilisent la technique du '''prédécodage''', où les instructions sont partiellement décodées avant d'entrer dans le cache d'instruction. Le prédécodage facilite grandement le travail des décodeurs d'instruction proprement dit. Par contre, le prédécodage prend de la place dans le cache L1 d'instruction, une partie de sa capacité est utilisé pour mémoriser les informations prédécodées. C'est donc un compromis entre taille du cache et taille/rapidité des décodeurs d'instruction. Sur les architectures K5 et K6, le prédécodage précise, pour chaque octet, si c'est le début ou la fin d'une instruction, si c'est un octet d'opcode, en combien de micro-opérations sera décodée l'instruction, etc. A partir de l'AMD K7, le prédécodage reconnait les branchements inconditionnels. Lorsqu'un branchement inconditionnel est pré-décodé, le pré-décodage tient compte du branchement et continue le pré-décodage des instructions à partir de la destination du branchement. Le système de prédécodage est abandonnée à partir de l'architecture Bulldozer, qui suit l'architecture K10. La prédiction de branchement de ces CPU tire partie de ce système de pré-décodage, à savoir que les prédictions de branchement sont partiellement mémorisées dans les lignes de cache du L1 d'instruction. Par exemple, l'AMD K5 se passe de ''Branch Target Buffer'' grâce à cela. Si une ligne de cache contient un branchement, elle mémorise l'adresse de destination de ce branchement, en plus des bits de pré-décodage. Si il y a plusieurs branchements dans une ligne de cache, c'est l'adresse de destination du premier branchement pris dans cette ligne de cache qui est mémorisée. Un défaut de cette approche est que si le branchement n'est pas dans le L1 d'instruction, aucune prédiction de branchement ne peut être faite et le préchargement ne peut pas fonctionner. C'est une limitation que n'ont pas les BTB découplées du cache L1 : elles peuvent prédire un branchement qui a été évincé dans le L2 ou le L3, tant que l'entrée associée est dans le BTB. Les prédictions peuvent même servir à précharger les instructions utiles. [[File:Comparaison du chargement de l'AMD K5 et K6.png|centre|vignette|upright=2|Comparaison du chargement de l'AMD K5 et K6]] Au niveau du décodage, on trouve de nombreuses différences entre les premières architectures AMD. L'AMD K5 contient 4 décodeurs hybrides, afin de décoder 4 instructions par cycles. Le K5 a quatre décodeurs simples couplés à 4 décodeurs complexes avec chacun un accès au micro-code. Une instruction peut donc passer par a donc deux voies de décodage : un décodage rapide et simple pour les instructions simples, un décodage lent et passant par le microcode pour les instructions complexes. Pour décoder 4 instructions, les deux voies sont dupliquées en 4 exemplaires, ce qui a un cout en circuits non-négligeable. L'AMD K6 utilise moins de décodeurs et ne peut que décoder deux instructions à la fois maximum. Par contre, il fournit en sortie 4 micro-opérations. Il intègre pour cela deux décodeurs simples, un décodeur complexe et un décodeur micro-codé. Un décodeur simple transforme une instruction simple en une ou deux micro-opérations. Il est possible d'utiliser les deux décodeurs simples en même temps, afin de fournir 4 micro-opérations en sortie du décodeur. Les deux autres décodent une instruction complexe en 1 à 4 micro-opérations. Si jamais la ou les deux instructions sont décodées en 1, 2 ou 3 micro-opérations, les micro-opérations manquantes pour atteindre 4 sont remplies par des NOPs. Pour le K7 et au-delà, le processeur dispose de décodeurs séparées pour les instructions micro-codées de celles qui ne le sont pas. Le processeur peut décoder jusqu’à 3 instructions par cycle. Le décodage d'une instruction microcodée ne peut pas se faire en parallèle du décodage non-microcodé. C'est soit le décodeur microcodé qui est utilisé, soit les décodeurs câblés, pas les deux en même temps. Le décodage d'une instruction prend 4 cycles. Les instructions non-microcodées sont décodées en une seule micro-opération, à un détail près : le CPU optimise la prise en charge des instructions ''load-up''. La différence entre le K6 et le K7 s'explique par des optimisations des instructions ''load-up''. Sur le K6, les instructions ''load-up'' sont décodées en deux micro-opération : la lecture en RAM, l'opération proprement dite. Mais sur le K7, une instruction ''load-up'' est décodée en une seule micro-opération. En conséquence, les décodeurs simples sont fortement simplifiés et le décodeur complexe disparait au profit d'un microcode unique. [[File:Décodage sur le K5 et le K5.png|centre|vignette|upright=3|Décodage sur le K5 et le K5]] ====Les micro-architectures K5 et K6 d'AMD==== Les deux premières architectures étaient les architectures K5 et K6, l'architecture K6 ayant été déclinée en quatre versions, nommées K6-1, K6-2, et K-3, avec une version K6-3 bis. Elles sont regroupées ensemble car elles ont beaucoup de points communs. Par exemple, tout ce qui a trait au chargement et au cache était similaire, de même que les unités de calcul. Les deux architectures avaient n'avaient pas de cache L2 et devaient se contenter d'un cache L1 d'instruction et d'un cache L1 de données. L'AMD K5 incorpore une TLB unique, alors que le K6 utilise des TLB séparées pour le cache d'instruction et le cache de données. Une différence entre l'architecture K5 et K6 est que la première utilise des caches normaux, alors que la seconde utilise des ''sector caches''. Les deux architectures disposaient des unités de calcul suivantes : deux ALU entières, une FPU, deux unités LOAD/STORE pour les accès mémoire, une unité de branchement et une ou plusieurs unités SIMD. Une organisation classique, donc. Pour les unités entières, il y avait deux ALU simples, un ''barrel shifter'' et un diviseur. Il n'y a pas d'erreur, le processeur incorpore un circuit diviseur, mais pas de circuit multiplieur. La raison est que la multiplication est réalisée par la FPU ! En effet, le multiplieur flottant de la FPU intègre un multiplieur entier pour multiplier les mantisses, qui est utilisé pour les multiplications entières. La même technique a été utilisée sur l'Atom, comme vu plus haut. Le tout était alimenté par deux ports d'émission, appelés ports X et Y. Sur l'architecture K5, le ''barrel shifter'' et le diviseur sont des ports différents. {|class="wikitable" |+ AMD K5 |- ! Port X ! Port Y |- | ALU simple | ALU simple |- | ''Barrel Shifter'' | Diviseur |} Sur l'architecture K6, le ''barrel shifter'' et le diviseur sont sur le même port. {|class="wikitable" |+ AMD K6 |- ! Port X ! Port Y |- | ALU simple | ALU simple |- | | ''Barrel Shifter'' |- | | Diviseur |} Niveau unités mémoire, le K5 avait deux unités LOAD/STORE, chacune capable de faire lecture et écriture. Par contre, la ''store queue'' n'a qu'un seul port d'entrée, ce qui fait que le processeur peut seulement accepter une écriture par cycle. Le processeur peut donc émettre soit deux lectures simultanées, soit une lecture accompagnée d'une écriture. Impossible d'émettre deux écritures simultanées, ce qui est de toute façon très rare. L'architecture K6 utilise quant à elle une unité LOAD pour les lectures et une unité STORE pour les écritures. Ce qui permet de faire une lecture et une écriture par cycle, pas autre chose. Niveau unités SIMD, l'architecture K7 n'avait qu'une seule unité SIMD, placée sur le port d'émission X. L'architecture K8 ajouta une seconde unité SIMD, sur l'autre port d'émission entier. De plus, trois ALU SIMD ont été ajoutées : un décaleur MMX, une unité 3DNow!, une unité mixte MMX/3DNow. Elles sont reliées aux deux ports d'émission entier X et Y ! Elles ne sont pas représentées ci-dessous, par souci de simplicité. [[File:Unité de calcul des processeurs AMD K5 et K6.png|centre|vignette|upright=2|Unité de calcul des processeurs AMD K5 et K6. les unités sur la même colonnes sont reliées au même port d'émission.]] Si les unités de calcul et le chargement sont globalement les mêmes, les deux architectures se différencient sur l'exécution dans le désordre. L'AMD K5 utilise du renommage de registre dans le ROB avec des stations de réservation. Par contre, l'AMD K6 utilise une fenêtre d'instruction centralisée. De plus, son renommage de registre se fait avec un banc de registre physique. L'architecture AMD K5 utilisait de deux stations de réservation par unité de calcul, sauf pour les deux unités mémoire partageaient une station de réservation unique (deux fois plus grande). Les stations de réservation sont cependant mal nommées, vu que ce sont en réalité des mémoire FIFO. Une micro-opération n'est émise que si elle est la plus ancienne dans la FIFO/station de réservation. Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique. Le tampon de ré-ordonnancement faisait seulement 16 instructions. [[File:AMD K5.jpg|centre|vignette|upright=3|AMDK5 Diagramme.]] L'architecture K6 remplace les stations de réservations par une fenêtre d'instruction centralisée. Les 4 micro-opérations renommées sont écrites dans la fenêtre d'instruction par groupe de 4, NOP de ''padding'' inclus. La fenêtre d'instruction centralisé contient 24 micro-opérations, groupées en 6 groupes de 4 micro-opérations, avec potentiellement des NOP dedans suivant le résultat du décodage. L'avantage est que l'implémentation de la fenêtre d'instruction est simple. La fenêtre d'instruction centralisée permettait d'émettre 6 micro-opérations en même temps (une par unité de calcul/mémoire). Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique. Le processeur utilisait un renommage avec un banc de registre physique. Le banc de registre physique pour les entiers contenait 48 registres, dont 24 étaient des registres architecturaux et 24 étaient des registres renommés. Sur les 24 registres architecturaux, 16 avaient une fonction de ''scratchpad'' que les ''datasheets'' d'AMD ne détaillent pas, les 8 restants étaient les registres généraux EAX, EBX, etc. [[File:AMD K6 Little foot & Modl 6.png|centre|vignette|upright=3|AMD K6 original.]] ====Les micro-architectures K7, K8 et K10 d'AMD==== Les micro-architectures suivantes sont les architectures K7, K8 et K10. Les architectures K7, K8 et K10 sont assez similaires. La différence principale entre le K7 et le K8 est le support du 64 bits. Les apports du K10 sont la présence d'un cache L3, d'une unité de calcul supplémentaire et d'améliorations de la prédiction de branchement. La taille de certains caches a été augmentée, de même que la largeur de certaines interconnexions/bus. L'architecture K7 des processeurs Athlon utilisait le renommage de registre, mais seulement pour les registres flottants, pas pour les registres entiers. Le ranommeg des registres flottants étaient réalisé via un banc de registres physique, ne contenant que des registres flottants. Les architectures K8 et K10 utilisent le renommage de registres pour tous les registres, entiers comme flottants. Par contre, le renommage de registre n'est pas réalisé de la même manière pour les registres entiers et flottants. Les registres entiers sont renommés dans le tampon de ré-ordonnancement, comme c'était le cas sur les architectures Intel avant le Pentium 4. Par contre, les registres flottants sont renommés grâce à un banc de registre physique. Le K8 est donc un processeur au renommage hybride, qui utilise les deux solutions de renommage principales. A partir du K7, le CPU optimise la prise en charge des instructions ''load-up''. Les instructions ''load-op'' sont appelées des macro-opérations dans la terminologie d'AMD, et aussi d'Intel. L'idée est que les instructions ''load-up'' sont décodées en micro-opérations intermédiaires. Elles sont propagées dans le pipeline comme étant une seule micro-opération, jusqu'à l'étage d'émission. Lors de l'émission, les instructions ''load-up'' sont scindées en deux micro-opérations : la lecture de l'opérande, puis l'opération proprement dite. Faire ainsi économise des ressources et optimise le remplissage du tampon de ré-ordonnancement, des fenêtres d'instructions, des stations de réservation, etc. Le tampon de réordonnancement est combiné avec divers circuits en charge de l'exécution dans le désordre, dans ce qui s'appelle l'''instruction control unit''. Il contient de 72 à, 84 instructions, qui sont regroupées en groupes de 3. Là encore, comme pour le K5 et le K6, le tampon de réordonnancement tient compte de la sortie des décodeurs. Les décodeurs fournissent toujours trois micro-opérations par cycle, quitte à remplir les vides par des NOP. Le tampon de réordonnancement reçoit les micro-opérations, NOP inclus, par groupes de 3, et est structuré autour de ces triplets de micro-opération, y compris en interne. Pour ce qui est de l'unité mémoire, elle est précédée par une file de µops mémoire, qui émet les accès mémoire dans l'ordre du programme. Elle est souvent qualifiée de ''Load-Store Queue'', mais ce n'est pas la terminologie que nous utilisons dans ce cours. La file de micro-opération lire/écrire 64 bits par cycle depuis le cache L1, ce qui fait un seul accès au cache par cycle. La file de µops mémoire est appelée la ''Pre-Cache Queue''. Si au vu de son nom, vous avez deviné qu'il y avait une ''Post-Cache Queue''. Elle mémorise les lectures/écritures émises, mais qui ont levé un défaut de cache L1. Elle ne fait pas partie de la file de µops mémoire proprement dite. Les architectures K7, K8 et K10 ont des unités de calcul très similaires. Concrètement, il y a trois ALU entières, trois unités de calcul d'adresse, et une FPU. Le processeur incorpore, aussi un multiplieur entier, relié sur le port d'émission de la première ALU. La FPU regroupe un additionneur flottant, un multiplieur flottant, et une troisième unité LOAD/STORE pour les lectures/écritures pour les nombres flottants. L'architecture K8 ajoute une unité de manipulation de bit, la K10 un diviseur entier. [[File:Unité de calcul des processeurs AMD K7, K8 et K10.png|centre|vignette|upright=2|Unité de calcul des processeurs AMD K7, K8 et K10]] La manière d'alimenter les ALU en micro-opérations varie un petit peu entre les architectures K7, K8 et K10. Il y a cependant quelques constantes entre les trois. La première est qu'il y a une fenêtre d'instruction séparée pour les flottants, de 36 à 42 entrées, avec renommage de registre. La fenêtre d'instruction flottante a trois ports d'émission : un pour l'additionneur flottant, un autre pour le multiplieur, et un troisième pour la troisième unité flottante qui s'occupe du reste. La seconde est que chaque ALU entière est couplée avec une unité de calcul d'adresse. Par contre, la méthode de couplage varie d'un processeur à l'autre. : Les stations de réservation sont nommées des ''schedulers'' dans les schémas qui suivent. La micro-architecture K7 avait deux fenêtres d'instruction : une pour les opérations flottantes, une autre pour les instructions entières et les accès mémoire. La fenêtre d'instruction entière était reliée à 3 ALU entières et à 3 AGU. Elle pouvait émettre trois micro-opérations en même temps : trois micro-opérations entières, trois micro-opérations mémoire. Les AGU étaient reliées à la file de µops mémoire mentionnée plus haut, ce qui permet d'émettre trois µops mémoire par cycle. Par contre, la file de µops mémoire ne pouvait exécuter qu'une lecture de 64 bits ou une écriture de 64 bits. En clair, trois micro-opérations mémoire peuvent être émises par cycle, cela entraine trois calculs d'adresse simultanés, mais les trois lectures/écritures sont mises en attente dans la file de µops mémoire. Elles s'exécutent alors l'une après l'autre. La fenêtre d'instruction entière contenait 5 à 6 groupes de 3 macro-opérations. Vous noterez que j'ai parlé de macro-opérations et pas de micro-opérations, car les instructions ''load-up'' sont considérées comme une seule "micro-opération" dans la fenêtre d'instruction entière. Et cela se marie bien avec une fenêtre d'instruction unique partagée entre pipeline entier et pipeline mémoire. Une macro-opération était scindée en deux micro-opérations : une micro-opération mémoire et une micro-opération entière. Il est donc avantageux de regrouper unités mémoire et unités entières à la même fenêtre d'instruction pour ce faire. [[File:AMD K7.png|centre|vignette|upright=3|AMD K7]] Sur les architectures K8 et K10, la station de réservation unique de 15 micro-opérations est remplacée par trois stations de réservations, de 8 micro-opérations chacune pour le K8, de 10 pour le K10. Chaque station de réservation entière alimente une unité de calcul entière et une unité de calcul d'adresse. l'unité de calcul d'adresse est reliée à la file de µops mémoire, qui n’exécute toujours qu'un seul accès mémoire par cycle. Le multiplieur est relié à la première station de réservation, sur le même port d'émission que l'ALU. [[File:AMD Husky microarchitecture.png|centre|vignette|upright=3|AMD Husky micro-architecture]] La micro-architecture K10 a été déclinée en plusieurs versions, nommées Grayhound, Grayhound+ et Husky, Husky étant une architecture gravée en 32 nm dédiée aux processeurs A-3000. L'architecture Grayhound a plus de cache et un ROB plus grand, la Husky est quand à elle un peu plus différente. Elle n'a pas de cache L3, contrairement aux autres architectures K10, ce qui simplifie fortement son sous-système mémoire. Par contre, les fenêtres d'instructions/stations de réservation et le ROB sont plus grands, pareil pour les files dans l'unité mémoire. Une ALU pour les divisions entières a aussi été ajoutée. Pour résumer, les architectures K7, K8 et K10 séparent les pipelines entiers et flottants : trois pipelines entiers avec chacun son unité de calcul, et un pipeline flottant avec plusieurs unités de calcul. Les raisons à cela sont assez diverses. Disons que dupliquer des ALU entières simples prend peu de transistors, là où les gros circuits comme le multiplieur ou la FPU ne sont pas dupliqués. Et cela a un autre avantage : le renommage, ''dispatch'' et l'émission sont plus simples. Les pipelines entiers ont une exécution dans le désordre peu complexe, grâce au grand nombre d'unités de calcul, ce qui fait que le pipeline entier est de seulement 15 cycles au total (chargement et décodage inclus). A l'opposé, la FPU est alimentée par une exécution dans le désordre très complexe, avec banc de registre physique et beaucoup de ressources, mais au prix d'un pipeline flottant plus long de 3 cycles, soit 18 cycles au total. ===Les micro-architectures ZEN d'AMD=== Viennent ensuite les '''micro-architectures Bulldozer''', avec trois révisions ultérieures nommées Piledriver, Steamroller et Excavator. Mais du fait de l'utilisation de techniques de multithreading matériel que nous n'avons pas encore abordé, nous ne pouvons pas en parler ici. Les micro-architectures suivantes sont les '''architectures ZEN 1/2/3/4/5'''. Elles se ressemblent beaucoup, chacune accumulant les améliorations des précédentes. Mais le cœur de l'architecture reste plus ou moins le même. En passant à la suivante, le nombre de registre virtuel augmente, le ''branch target buffer'' augmente en taille, le ROB et les files d'attente grossissent, les caches de micro-opération aussi, les caches grossissent, etc. Une optimisation intéressante est l'ajout d'un cache de micro-opération, qui améliore grandement les performances du ''front-end'', notamment pour les boucles. La micro-architecture Zen 1 est illustrée ci-dessous. Comme on le voit, les registres flottants ont une unité de renommage séparée de celle pour les entiers, mais les deux utilisent du renommage à banc de registre physique. Il y a par contre une différence au niveau des fenêtres d'instruction, notées ''scheduler'' dans le schéma. Pour ce qui est des unités de calcul flottantes, il y a une fenêtre unifiée qui alimente quatre ALU, grâce à 4 ports d'émission. Mais pour les ALU entières, il y a une fenêtre d'instruction par ALU, avec un seul port d'émission connecté à une seule ALU. La raison de ce choix est que les opérations flottantes ont un nombre de cycle plus élevé, sans compter que les codes flottants mélangent bien additions et multiplication. Une fois décodées, les instructions sont placées dans une première file de micro-opérations om elles attendent, puis sont dispatchées soit dans le pipeline entier, soit dans le pipeline flottant. les micro-opérations entières sont insérées dans une fenêtre d'instruction directement, alors que les micro-opérations flottantes doivent patienter dans une seconde file de micro-opérations. La raison est que les micro-opérations flottantes ayant une grande latence, trop d'instructions flottantes consécutives pourraient bloquer le pipeline flottant, sa fenêtre d'instruction étant pleine. Le pipeline flottant étant bloqué, la première file de micro-opérations serait bloquée et on ne pourrait plus émettre de micro-opérations entières. Pour éviter cela, une solution serait d'agrandir la file de micro-opérations, mais cela la rendrait plus lente et se ferait donc au détriment de la fréquence d'horloge. Alors une solution a été d'ajouter une seconde file de micro-opérations, au lieu d'agrandir la première. [[File:Zen microarchitecture.svg|centre|vignette|upright=3|Micro-architecture Zen 1 d'AMD.]] Le passage à la micro-architecture n'a pas causé de grands changements. Le Zen 2 a ajouté une unité de calcul d'adresse, ce qui fait qu'on passe à 4 ALU, 3 AGU et 4 FPU. La fenêtre d'instruction flottante reste la même. Par contre, les fenêtres d'instruction entières changent un peu. Ou plutôt devrais-je dire les fenêtres d'instruction mémoire. En effet, le Zen 2 fusionne les fenêtres d'instructions liées aux AGU en une seule fenêtre d'instruction deux fois plus grosse. Le Zen 5 a ajouté deux autres ALU entières et une unité de calcul d'adresse (6 ALU / 4 AGU) ==Les processeurs Atom d'Intel, de microarchitecture Bonnell== L'architecture de l'Atom première génération est assez simple. Son pipeline faisait 16 étages, ce qui est beaucoup. C'est un processeur 32 bits, ce qui aura son importance dans ce qui suit. Il était conçu pour être un processeur basse consommation, donc peu puissant. En conséquence, il n'a pas d'exécution dans le désordre, même s'il est superscalaire. C'était la norme à l'époque pour les processeurs basse consommation, que de faire sans exécution dans le désordre. De nos jours, les choses ont bien changée, même les processeurs basse consommation ont exécution dans le désordre, superscalarité et renommage de registres. ===Le ''front-end'' de l'Atom=== Le cache d'instruction permet de lire 8 octets par cycle, qui sont placés dans une file d'instruction, elle-même suivie par deux décodeurs. Le fait que les décodeurs lisent les instructions depuis une file d'instruction fait que les deux instructions décodées ne sont pas forcément consécutives en mémoire RAM. Par exemple, l'Atom peut décoder un branchement prédit comme pris, suivi par l'instruction de destination du branchement. Les deux instructions ont été chargées dans la file d'instruction et sont consécutifs dedans, alors qu'elles ne sont pas consécutives en mémoire RAM. Sur l'Atom, la majorité des instructions x86 sont décodées en une seule micro-opération, y compris les instructions ''load-up''. Le microcode n'est utilisé que pour une extrême minorité d'instructions et est à part des deux décodeurs précédents. L'avantage est que cela permet d'utiliser au mieux la file de micro-opération, qui est de petite taille. Mais surtout, cela permet de grandement réduire la consommation du processeur, au détriment de ses performances. Pour avoir un décodage rapide, malgré des instructions complexes, le processeur recourt à la technique du pré-décodage, qui prédécode les instructions lors de leur chargement dans le cache d'instruction. Le prédécodage lui-même prend deux cycles, là où une lecture dans le L1 d'instruction en prend 3. les défauts de cache d'instruction sont donc plus longs de deux cycles. Mais l'avantage du prédécodage est que la consommation d'énergie est diminuée. Prenez une instruction exécutée plusieurs fois, dans une boucle. Au lieu de décoder intégralement une instruction à chaque fois qu'on l'exécute, on la prédécode une fois, seul le reste du décodage est fait à chaque exécution. D'où un gain d'énergie assez intéressant. Les caches de micro-opération, qui sont capables d'exécuter une optimisation similaire, n'existaient pas encore à cette époque. ===Le chemin de données de l'Atom=== Les deux décodeurs alimentent une file de micro-opérations de petite taille : 32 µops maximum, 16 par ''thread'' si le ''multithreading'' matériel est activé. La file de micro-opérations a deux ports d'émission, ce qui permet d'émettre au maximum 2 µops par cycle. Les conditions pour cela sont cependant drastiques. Les deux instructions ne doivent pas avoir de dépendances de registres, à quelques exceptions près liées au registre d'état. Le multithreading matériel doit aussi être désactivé. Les deux instructions doivent aller chacun dans un port différent, et cela tient en compte du fait que les deux ports sont reliés à des unités de calcul fort différentes. Le tout est illustré ci-dessous. Les deux ports ont chacun une ALU simple dédiée, capable de faire des additions/soustractions, des opérations bit à bit et des copies entre registres. Mais ils ont aussi des opérations qui leur sont spécifiques. La séparation entre les deux pipelines est assez complexe. Il ne s'agit pas du cas simple avec un pipeline entier et un pipeline flottant séparés. En réalité, il y a deux pipelines, chacun capables de faire des opérations entières et flottantes, mais pas les mêmes opérations. Le premier port permet d’exécuter des opérations entières simples, une addition flottante, des comparaisons/branchements, ou une instruction de calcul d'adresse LEA. Le second port/pipeline est, quant à lui, conçu pour exécuter les instruction ''load-up'' nativement, en une seule micro-opération. Il contient toute la machinerie pour faire les accès mémoire, notamment des unités de calcul d'adresse et un cache L1 de données. A la suite du cache, se trouvent une ALU entière simple, un ''barrel shifter'', et un circuit multiplieur/diviseur. Le circuit multiplieur/diviseur est utilisé à la fois pour les opérations flottantes et entières. [[File:Intel Atom Microarchitecture.png|centre|vignette|upright=2.5|Intel Atom Microarchitecture]] Cette organisation difficile à comprendre est en réalité très efficace, très économe en circuit, tout en gardant une performance intéressante. Les instructions simples, ADD/SUB/bitwise sont supportées dans les deux pipelines. Il faut dire que ce sont des opérations courantes qu'il vaut mieux optimiser au mieux. Le processeur peut donc émettre deux opérations simples et fréquentes en même temps, ce qui augmente les performances. Les opérations plus complexes, à savoir les multiplications/divisions/décalages/rotations/manipulations de bit sont supportées dans un seul pipeline. La raison est qu'il est rare que de telles opérations soient consécutives, et qu'il n'est donc pas utile d'optimiser pour cette situation. Si les deux pipelines devaient supporter ces opérations, cela demanderait de dupliquer les circuits multiplieurs/diviseur, ce qui aurait un cout en circuit important pour un gain en performance assez faible. ===Le système d'exceptions flottantes de l'Atom=== Le processeur étant sans exécution dans le désordre, ses instructions doivent écrire dans les registres dans l'ordre du programme. En conséquence, certaines instructions doivent être retardées, leur émission doit attendre que les conditions soient adéquates. Et cela pose problème avec les opérations flottantes, vu qu'elles prennent pas mal de cycles pour s'exécuter. Imaginez qu'une instruction flottante de 10 cycles soit suivie par une instruction entière. En théorie, on doit retarder l'émission de l'instruction entière de 9 cycles pour éviter tout problèmes. Le cout en performance est donc assez important. En théorie, les instructions entières et flottantes écrivant dans des registres séparés, ce qui fait que l'on pourrait exécuter instructions entières et flottantes dans le désordre. Sauf pour les instructions de copie entre registres entier et flottants, mais laissons-les de côté. Le problème est qu'une instruction flottante peut parfois lever une exception, par exemple en cas de division par zéro, ou pour certains calculs précis. Si une exception est levée, alors l'instruction flottante est annulée, de même que toutes les instructions qui suivent, y compris les opérations entières. Ce n'est pas un problème si le processeur gère nativement les exceptions précises, par exemple avec un tampon de ré-ordonnancement. Mais l'Atom étant un processeur sans exécution dans le désordre, les instructions entières devraient être mises en attente tant qu'une instruction flottante est en cours d'exécution. Heureusement, l'Atom d'Intel a trouvé une parade. La technique, appelée ''Safe Instruction Recognition'' par Intel, est décrite dans le brevet US00525721.6A. L'idée est de tester les opérandes flottantes, pour détecter les combinaisons d'opérandes à problème, dont l'addition/multiplication peut lever une exception. Si des opérandes à problème sont détectées, on stoppe l'émission de nouvelles instructions en parallèle de l'instruction flottante et l'unité d'émission émet des bulles de pipeline tant que l'instruction flottante est en cours. Sinon, l'émission multiple fonctionne. La technique permet ainsi de ne pas écrire dans les registres entiers/flottants dans l'ordre du programme : une instruction entière peut être autorisée à s'exécuter même si elle écrit dans un registre entier avant qu'une instruction flottante délivre son résultat. ==La microarchitecture Netburst du Pentium 4== Dans cette section, nous allons voir l'architecture du processeur Pentium 4, qu'on a volontairement laissée de côté précédemment. Pourquoi un tel saut dans le temps ? Parce que le Pentium est complément à part des autres architectures Intel. Le Pentium 4 a représenté une rupture en termes de microarchitecture, qui a été un échec tellement retentissant que les processeurs suivants sont repartis sur la base du Pentium 3. Il introduisait de nombreuses nouveautés architecturales qui étaient très innovantes. Par exemple, il introduisait le renommage avec un banc de registre physique, qui a été utilisé sur tous les processeurs Intel suivants. Mais la plupart de ces innovations étaient en réalité de fausses bonnes idées, ou du moins des idées difficiles à exploiter. Par exemple, le système de pipeline à ''replay'' n'a été utilisé que sur le Pentium 4 et aucun autre processeur ne l'a implémenté. ===Un focus sur la fréquence d'horloge=== La microarchitecture du Pentium 4 a été déclinée en plusieurs versions, dont les finesses de gravure n'étaient pas les mêmes. La microarchitecture Netburst, utilisée sur le Pentium 4, utilisait un pipeline à 20 étage, augmenté à 32 sur une révision ultérieure. Il a existé quatre révisions de l'architecture : Willamette (180 nm), Northwood (130 nm), Prescott (90 nm) et Cedar Mill (65 nm). Un point important est que le Pentium 4 était prévu pour fonctionner à haute fréquence. Ses 1,5 GHz étaient impressionnants pour l'époque, les autres processeurs tournant à une fréquence proche du GigaHertzs. Pour cela, la solution retenue par Intel a été un pipeline très long, avec beaucoup d'étages. Le Pentium 4 a été décliné en plusieurs versions assez proches, chacune avec sa propre finesse de gravure qui n'ont pas toute le même pipeline. Les micro-architectures ''Willamette'' et ''Northwood'' avaient un pipeline de 20 étages, alors que les autres processeurs de l'époque avaient entre 10 et 15 étages maximum. Les micro-architectures ''Prescott'' et ''Cedar Mill'' étaient une refonte qui a fait grimper le nombre d'étages à 31 ! Du jamais vu, il s'agit d'un record pour un processeur commercial. Un pipeline aussi long permet d'exécuter beaucoup d’instructions en même temps, chacune dans un étage, mais aussi d'atteindre de hautes fréquences facilement. Le problème est qu'un pipeline avec autant d'étages a beaucoup de problèmes. Un point important est que la prédiction de branchement est cruciale. Pour rappel, la pénalité en cas de mauvaise prédiction dépend du nombre d'étages avant que le branchement soit résolu. Et les branchements sont résolus soit en fin de décodage, soit dans l'unité de calcul. C'est à dire au milieu du pipeline, soit en fin de pipeline. La pénalité en cas de mauvaise prédiction de branchement était énorme sur le Pentium 4, elle atteignait facilement 30 cycles Pour compenser, le Pentium 4 avait une prédiction de branchement très performante, pour l'époque. J'insiste sur le pour l'époque. Il utilisait un prédicteur qu'on a déjà abordé dans le chapitre sur la prédiction de branchement, précisément un prédicteur adaptatif à deux niveaux avec un historique global de 16 bits. Il avait aussi un ''Branch Target Buffer'' de 4096 entrées. Mais surtout, il intégrait une sorte de précurseur du cache de micro-opération, appelé le cache de traces, qui est détaillé dans la section suivante. ===Le cache de trace du Pentium 4=== Les décodeurs du Pentium 4 ne font pas décoder les instructions, ils mémorisent le résultat dans un cache de micro-opération un peu particulier, appelé le '''cache de trace'''. Une ligne de cache peut mémoriser 6 micro-opérations, ce qui peu sembler peu mais a été repris sur les micro-architectures suivantes. Mais le cache de trace a une grande différence avec un cache de micro-opération normal. Un cache de micro-opération normal mémorise une instruction par ligne de cache. Une instruction est décodée en plusieurs micro-instructions, qui sont enregistrées dans une ligne de cache. Si l'instruction n'utilise par les 6 micro-opérations disponibles, le reste de la ligne de cache n'est pas utilisé. Mais le Pentium 4 optimise le tout de manière ce à ce que ne soit pas le cas. Sur le Pentium 4, la contrainte du "une instruction par ligne de cache" est abandonnée. Une ligne de cache mémorise 6 micro-opérations consécutives, qui peuvent appartenir à plusieurs instructions. Par exemple, si le décodeur décode 4 instructions consécutives en 6 micro-opérations au total, alors le tout prendra une seule ligne de cache sur le Pentium 4. Et les 4 instructions consécutives n'ont même pas à être consécutives en mémoire : il peut y avoir des branchements pris entre ces instructions ! {|class="wikitable" |+ Cache de trace |- ! Ligne de cache | ADD || SUB || ADD || MOV || MUL || ''shift'' |- ! Ligne de cache | colspan="3" | ADD ''load-up'' || MUL || colspan="2" | Branch if Equal |- ! Ligne de cache | XOR || colspan="4" | POP || SUB |- ! ... | colspan="6" | ... |} Pour expliquer cela plus concrètement, nous allons devoir introduire les concepts de trace et de bloc de base. Un '''bloc de base''' (''basic block'') est une suite d'instructions sans branchement, qui est séparé par deux branchements. Le début d'un bloc de base est la destination d'un branchement, un bloc de base se termine avec un branchement. Une '''trace''' est formée en concaténant plusieurs blocs de base. Pour donner un exemple, regardez le code illustré ci-contre. Il est composé d'un bloc de base A, suivi par un bloc de base B, qui peut faire appel soit au bloc C, soit un bloc D. Un tel code peut donner deux traces : ABC ou ABD. La trace exécutée dépend du résultat du branchement qui choisit entre C et D. Le cache de trace mémorise des traces de 6 micro-opérations consécutives. Les traces sont formées en sortie des décodeurs d'instruction, par de subtiles opérations mélangeant mémorisation, décalage et concaténation. Les circuits qui construisent les traces ne sont pas connus, mais ils doivent certainement être très compliqués. toujours est-il qu'un cache de trace peut mémoriser des traces différentes, même si leur début est le même. Par exemple, prenons deux traces, composées des blocs de base A, B, C et D. La première trace est la trace ABC, la seconde est la trace ABD. Les deux traces auront chacune une ligne de cache dédiée. Une trace est réutilisable quand le premier bloc de base est identique et que les prédictions de branchement restent identiques. Pour vérifier cela, le tag du cache de traces contient l'adresse du premier bloc de base, la position des branchements dans la trace et le résultat des prédictions utilisées pour construire la trace. Le résultat des prédictions de branchement de la trace est stocké sous la forme d'une suite de bits : si la trace contient n branchements, le n-ième bit vaut 1 si ce branchement a été pris, et 0 sinon. Même chose pour la position des branchements dans la trace : le bit numéro n indique si la n-ième instruction de la trace est un branchement : si c'est le cas, il vaut 1, et 0 sinon. Si la trace est réutilisée par la suite, elle est lue depuis le cache de traces. Pour savoir si une trace est réutilisable, l'unité de chargement envoie le ''program counter'' au cache de traces, l'unité de prédiction de branchement fournit le reste des informations. Si on a un succès de cache de traces, et la trace est envoyée directement au décodeur. Sinon, la trace est chargée depuis le cache d'instructions et assemblée. Il faut signaler que le cache de trace avait sa propre unité de prédiction de branchement séparée de l'unité de prédiction de branchement normale. [[File:TraceCache.png|centre|vignette|upright=2|Cache de traces.]] Le cache de traces réduisait la longueur du pipeline en cas de succès de cache de trace. Quand les instructions étaient lues depuis le cache de trace, les étages avant le cache de trace ne sont pas utilisés, tout se passe comme s'ils étaient retirés du pipeline. C'est la même chose avec le cache de micro-opération des processeurs modernes, mais l'idée n'existait pas encore à l'époque. Le cache de trace mémorise des traces décodées, ce qui fait qu'un succès de cache de trace contournait non seulement le cache d'instruction, mais aussi les décodeurs. Le temps d'accès au cache de trace pouvait être assez élevé, même s'il était comparable au temps d'accès du cache d'instruction. Le cache de traces a depuis été remplacé par une alternative bien plus intéressante, le cache de micro-opérations, plus flexible et plus performant. Comparé à un cache de trace, la contrainte "une instruction par ligne de cache" simplifie grandement l'implémentation d'un cache de micro-opération. Ne parlons pas de la détection des succès de cache, qui demande d'utiliser les prédictions de branchement. Mais le vrai problème avec le cache de trace est tout autre. Il arrive souvent qu'une micro-opération soit présente dans plusieurs lignes de cache en raison du processus de construction des traces, chose impossible avec un cache de micro-opération. Et c'est un problème, qui réduit la capacité effective du cache de trace. Alors certes, une ligne de cache est plus remplie que sur un cache de micro-opération, on est certain que les 6 micro-opération par ligne de cache sont remplies. Mais la redondance réduit grandement cet avantage. ===L'exécution dans le désordre et le chemin de données du P4=== Le renommage de registres se fait avec un banc de registres physiques avec une table d'alias. Le Pentium 4 avait une exécution dans le désordre très limitée, basée sur la présence de deux files de micro-opération : une pour les accès mémoire, une autre pour les autres instructions. La seconde file regroupait opérations entières et flottantes, elles n'étaient pas séparées. Avec ces deux files, les instructions mémoire étaient exécutées dans l'ordre du programme, les instructions arithmétiques s'exécutaient aussi dans l'ordre du programme, mais une instruction arithmétique pouvait passer avant une instruction mémoire et inversement. L'avantage est que cela permettait de faire des lectures en avance, c'était une forme limitée de lecture non-bloquantes. Il s'agit bel et bien de deux files d'instructions, pas de fenêtres d'instruction ni de stations de réservation. Le Pentium 4 est le seul processeur commercial qui a utilisé des files de micro-opération séparées, tous les autres utilisent des fenêtres d'instruction : centralisées pour Intel, décentralisées pour AMD (en général). Le Pentium 2 et 3, bien qu'antérieurs, utilisait une station de réservation unique. Cela peut sembler être un retour en arrière, mais les files d'instructions sont bien plus larges : de 42 micro-opérations pour le Pentium 3, on passe à 120 micro-opérations pour le Pentium 4. Et vu la longueur du pipeline, qui fait qu'il y a plus d'instructions en vol, c'était une nécessité. Mais cela n'aurait pas été possible en utilisant des stations de réservation, pour des raisons de consommation électrique et/ou de budget en transistors, ce qui fait que passer à une file de micro-opération été la solution retenue. Pour les accès mémoire, le Pentium 4 utilisait donc une file de µops mémoire unique, couplée à une file d'écriture (non-représentée sur les schémas qui suivent). La file d'écriture du Pentium 4 était de 24 écritures maximum et gérait le ''Store-to-load forwarding''. Le processeur pouvait émettre une lecture et une écriture à chaque cycle. Il y avait un port d'émission pour les lectures et un autre pour les écritures, tous deux ayant chacun leur propre unité de calcul d'adresse. Le processeur contenait 3 ALU entières, 2 unités de calcul d'adresse et une FPU. La FPU était complétée par une unité pour faire des copies entre registres flottants, des opérations MOV. Pour les AGU, il y en avait une dédiée aux lectures, une autre pour les écritures. Le tout était relié aux ports d'émissions comme suit : [[File:Ports d'émission du Pentium 4.png|centre|vignette|upright=2.5|Ports d'émission du Pentium 4]] Le processeur utilisait deux réseaux de contournement séparés : un pour les opérations flottantes, un pour les opérations entières. Le réseau de contournement pour les opérations entières est aussi relié aux unités de calcul d'adresse. Jusque là, rien de surprenant, le chemin de données du processeur est assez classique. [[File:Architettura Pentium 4.png|centre|vignette|upright=3|Microarchitecture du Pentium 4.]] ===Les unités de calcul entières du Pentium 4=== Sur le Pentium 4, les ALU entières étaient cadencées à une fréquence double de celle du processeur. Les ALU entières pouvaient exécuter deux micro-opérations par cycle, ce qui fait que les ports d'émissions reliés aux ALU devaient eux aussi fonctionner à double fréquence. Pour faire la différence entre les deux fréquences, nous parlerons de fréquence/cycle processeur et de fréquence/cycle de l'ALU. Précisons que seules les ALU entières étaient à double fréquence, pas le multiplieur, pas le ''barrel shifter''. Pour simplifier, nous allons parler d'additionneur plutôt que de l'ALU entière, ce qui sera plus proche de la réalité. Et l'implémentation de l'additionneur du Pentium 4 était très innovante. L'additionneur pouvait exécuter deux additions par cycle, même si les deux additions ont une dépendance. Mais n'allez pas croire que l'implémentation était intuitive, avec un additionneur 32 bit basique très rapide. Non seulement l'additionneur fonctionnait à double fréquence, mais il était aussi pipeliné, avec un système de contournement interne ! Les additionneurs étaient pipelinées, d'une manière très simple. Une addition 32 bits était découpée en trois étapes : deux additions de 16 bits, une dernière étape pour mettre à jour le registre d'état. Pour cela, chaque additionneur était composé de deux additionneur 16 bits chacune, placées l'une après l'autre, avec un registre de pipeline entre les deux. L'additionneur prenait deux cycles d'horloge pour faire son travail : le premier cycle calculait les 16 bits de poids faible, le second calculait les 16 bits de poids fort lors du second cycle. Le tout est appelé '''addition étagée''' (''staggered add'') dans la documentation Intel. Une addition se fait donc en deux étapes, sauf que c'est compensé par le fait que l'additionneur fonctionnait à une fréquence double de celle du processeur ! Le résultat de ce fonctionnement franchement bizarre, est que les 16 bits de poids faible étaient calculés en une moitié de cycle processeur, alors que l'opération complète prenait un cycle. Deux additions consécutives s'exécutaient donc en 1 cycle et demi, alors qu'on aurait cru au premier abord que cela prendrait seulement un cycle. Si on fait les calculs, on s'apercoit que le rythme de croisière est cependant proche de 2 additions par cycle, bien qu'inférieur. 3 additions consécutives se font en deux cycles, 5 additions en 3 cycles, 7 en 4 cycles, etc. Et le Pentium 4 ajoutait un système de contournement interne à l'ALU. En clair, si une addition utilise le résultat de l'addition précédente, les deux peuvent s'exécuter en un cycle d'horloge et demi. Les 16 bits de poids faible de la première addition sont disponibles après un cycle ALU, ce qui permet de démarrer le calcul de la seconde addition au cycle suivant. ===Le ''replay pipeline''=== Le processeur est un processeur triple émission : il peut charger et décoder 3 µops par cycle, le ROB peut terminer 3 µops par cycle, etc. Pourtant, les ports d'émission peuvent émettre 6 instructions par cycle : 4 ports, dont deux à double fréquence. Une telle différence s'explique par l'usage d'un ''replay pipeline'', dont nous avons déjà parlé dans ce cours. Pour rappel, le Pentium 4 suppose que les lectures font tous un succès de cache L1. Si une opération arithmétique utilise la donnée lue comme opérande, le processeur l'émet immédiatement, l'opérande sera disponible une fois l'opération en entrée de l'ALU entière. Mais s'il s'est trompé, le processeur ré-exécute l'instruction après un temps d'attente de quelques cycles, pour se caler sur la latence du cache L2. Et si il y a un défaut de cache L2, l’instruction attend encore. Cela demande de ré-exécuter des instructions émises à tord, ce qui fait que le processeur doit avoir la capacité d'exécution pour. Ce pourquoi le processeur peut émettre 6 µops dans les unités de calcul : 3 µops normales et 3 µops ré-exécutées. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Les processeurs superscalaires | prevText=Les processeurs superscalaires | next=Les processeurs VLIW et EPIC | nextText=Les processeurs VLIW et EPIC }} </noinclude> rqg6fh6qpn20p2pf650l3nfhcgsdiyp Mathc initiation/0071 0 84517 772663 772661 2026-09-21T13:04:04Z Xhungab 23827 772663 wikitext text/x-wiki __NOTOC__ [[Catégorie:Mathc initiation (livre)]] [[Mathc initiation/a582#Transformées en Z usuelles, des signaux causales discrets :| Sommaire]] ==La fonction cosinus discret : g(n) = cos(kn)== '''La transformée en Z de g[n] est : G[z] = [z^2 - z cos(k)] / [z^2 - 2 z cos(k) + 1]''' g[n] = cos(kn) * cos(kn) = (e^(kni)+e^(-kni))/2 g[n] = (e^(kni) + e^(-kni))/2 g[n] = 1/2 ('''e^(kni)''' + e^(-kni)) f(n) = a^n F[z] = z/(z-a) G[z] = 1/2 '''(z/(z-e^(ki))''' + z/(z-e^(-ki)) f(n) = '''e^(kni)''' F[z] = '''z/(z-e^(ki))''' G[z] = '''z'''/2 (1/(z-e^(ki)) + 1/(z-e^(-ki)) Mis au même dénominateur. G[z] = z/2 (('''z'''-e^(-ki) + ('''z'''-e^(ki)) / ((z-e^(ki))(z-e^(-ki))) G[z] = z/2 ((2z-('''e^(-ki)+e^(ki)''')) / ((z-e^(ki))(z-e^(-ki))) '''Numérateur :''' G[z] = z/2 ((2z-('''e^(-ki)+e^(ki))''') / ... * '''e^(ki)+e^(-ki) = 2 cos(k)''' G[z] = z/2 ((2z - 2 cos(k)) /... G[z] = z (( z - cos(k)) /... G[z] = '''( z^2- z cos(k))''' /... '''Dénominateur :''' G[z] = ... / ( (z - e^(ki)) (z - e^(-ki)) ) Développons : G[z] = ... / ( (z^2 - z e^(-ki)-z e^(ki) + '''e^(ki)(e^(-ki)''') G[z] = ... / ( (z^2 - z e^(-ki)-z e^(ki) + 1) '''e^(ki)(e^(-ki) = 1''' G[z] = ... / ( (z^2 - z '''(e^(-ki)+e^(ki))''' + 1) * '''e^(ki)+e^(-ki) = 2 cos(k)''' G[z] = ... / ( (z^2 - z ('''2 cos(k)''') + 1) G[z] = ... / ('''z^2 - 2 z cos(k) + 1)''' '''Donc :''' G[z] = (z^2 - z cos(k) ) / (z^2 - 2 z cos(k) + 1) ==La fonction sinus discret : h(n) = sin(kn)== '''La transformée en Z de h[n] est :''' H[z] = z sin(k)]/[z^2-2z cos(k)+1 h[n] = sin(kn) * sin(kn) = (e^(kni)-e^(-kni))/2i h[n] = (e^(kni)-e^(-kni))/2i h[n] = 1/2i ('''e^(kni)'''-e^(-kni)) f(n) = a^n F[z] = z/(z-a) H[z] = 1/2i (z/(z-e^(ki)) - z/(z-e^(-ki)) f(n) = '''e^(kni)''' F[z] = '''z/(z-e^(ki))''' H[z] = z/2i (1/(z-e^(ki)) - 1/(z-e^(-ki)) Mis au même dénominateur. H[z] = z/2i ((z-e^(-ki) - (z-e^(ki)) / ((z-e^(ki))(z-e^(-ki))) H[z] = z/2i ( z-e^(-ki) - z+e^(ki)) / ((z-e^(ki))(z-e^(-ki))) H[z] = z/2i ((-e^(-ki)+e^(ki))/((z-e^(ki))(z-e^(-ki))) '''Numérateur :''' H[z] = z/2i ((-e^(-ki)+e^(ki)) / ... * e^(ki)-e^(-ki) = 2i sin(k) H[z] = z/2i (2i sin(k)) /... H[z] = '''z sin(k)''' /... '''Dénominateur :''' G[z] = ... / ( (z - e^(ki)) (z - e^(-ki)) ) Développons : G[z] = ... / ( (z^2 - z e^(-ki)-z e^(ki) + '''e^(ki)(e^(-ki)''') G[z] = ... / ( (z^2 - z e^(-ki)-z e^(ki) + 1) '''e^(ki)(e^(-ki) = 1''' G[z] = ... / ( (z^2 - z '''(e^(-ki)+e^(ki))''' + 1) * '''e^(ki)+e^(-ki) = 2 cos(k)''' G[z] = ... / ( (z^2 - z ('''2 cos(k)''') + 1) G[z] = ... / ('''z^2 - 2 z cos(k) + 1)''' '''Donc :''' H[z] = z sin(k) / (z^2 - 2 z cos(k) + 1) {{AutoCat}} ayb8v4ykix60bzg4cx8u8txq9s54ta1 772664 772663 2026-09-21T13:06:57Z Xhungab 23827 772664 wikitext text/x-wiki __NOTOC__ [[Catégorie:Mathc initiation (livre)]] [[Mathc initiation/a582#Transformées en Z usuelles, des signaux causales discrets :| Sommaire]] ==La fonction cosinus discret : g(n) = cos(kn)== '''La transformée en Z de g[n] est : G[z] = [z^2 - z cos(k)] / [z^2 - 2 z cos(k) + 1]''' g[n] = cos(kn) * cos(kn) = (e^(kni)+e^(-kni))/2 g[n] = (e^(kni) + e^(-kni))/2 g[n] = 1/2 ('''e^(kni)''' + e^(-kni)) f(n) = a^n F[z] = z/(z-a) G[z] = 1/2 '''(z/(z-e^(ki))''' + z/(z-e^(-ki)) f(n) = '''e^(kni)''' F[z] = '''z/(z-e^(ki))''' G[z] = '''z'''/2 (1/(z-e^(ki)) + 1/(z-e^(-ki)) Mis au même dénominateur. G[z] = z/2 (('''z'''-e^(-ki) + ('''z'''-e^(ki)) / ((z-e^(ki))(z-e^(-ki))) G[z] = z/2 ((2z-('''e^(-ki)+e^(ki)''')) / ((z-e^(ki))(z-e^(-ki))) '''Numérateur :''' G[z] = z/2 ((2z-('''e^(-ki)+e^(ki))''') / ... * '''e^(ki)+e^(-ki) = 2 cos(k)''' G[z] = z/2 ((2z - 2 cos(k)) /... G[z] = z (( z - cos(k)) /... G[z] = '''( z^2- z cos(k))''' /... '''Dénominateur :''' G[z] = ... / ( (z - e^(ki)) (z - e^(-ki)) ) Développons : G[z] = ... / ( (z^2 - z e^(-ki)-z e^(ki) + '''e^(ki)(e^(-ki)''') G[z] = ... / ( (z^2 - z e^(-ki)-z e^(ki) + 1) '''e^(ki)(e^(-ki) = 1''' G[z] = ... / ( (z^2 - z '''(e^(-ki)+e^(ki))''' + 1) * '''e^(ki)+e^(-ki) = 2 cos(k)''' G[z] = ... / ( (z^2 - z ('''2 cos(k)''') + 1) G[z] = ... / ('''z^2 - 2 z cos(k) + 1)''' '''Donc :''' G[z] = (z^2 - z cos(k) ) / (z^2 - 2 z cos(k) + 1) ==La fonction sinus discret : h(n) = sin(kn)== '''La transformée en Z de h[n] est :''' H[z] = z sin(k)]/[z^2-2z cos(k)+1 h[n] = sin(kn) * sin(kn) = (e^(kni)-e^(-kni))/2i h[n] = (e^(kni)-e^(-kni))/2i h[n] = 1/2i ('''e^(kni)'''-e^(-kni)) f(n) = a^n F[z] = z/(z-a) H[z] = 1/2i (z/(z-e^(ki)) - z/(z-e^(-ki)) f(n) = '''e^(kni)''' F[z] = '''z/(z-e^(ki))''' H[z] = z/2i (1/(z-e^(ki)) - 1/(z-e^(-ki)) Mis au même dénominateur. H[z] = z/2i ((z-e^(-ki) - (z-e^(ki)) / ((z-e^(ki))(z-e^(-ki))) H[z] = z/2i ( z-e^(-ki) - z +e^(ki)) / ((z-e^(ki))(z-e^(-ki))) H[z] = z/2i ((-e^(-ki) +e^(ki)) / ((z-e^(ki))(z-e^(-ki))) '''Numérateur :''' H[z] = z/2i ((-e^(-ki)+e^(ki)) / ... * e^(ki)-e^(-ki) = 2i sin(k) H[z] = z/2i (2i sin(k)) /... H[z] = '''z sin(k)''' /... '''Dénominateur :''' G[z] = ... / ( (z - e^(ki)) (z - e^(-ki)) ) Développons : G[z] = ... / ( (z^2 - z e^(-ki)-z e^(ki) + '''e^(ki)(e^(-ki)''') G[z] = ... / ( (z^2 - z e^(-ki)-z e^(ki) + 1) '''e^(ki)(e^(-ki) = 1''' G[z] = ... / ( (z^2 - z '''(e^(-ki)+e^(ki))''' + 1) * '''e^(ki)+e^(-ki) = 2 cos(k)''' G[z] = ... / ( (z^2 - z ('''2 cos(k)''') + 1) G[z] = ... / ('''z^2 - 2 z cos(k) + 1)''' '''Donc :''' H[z] = z sin(k) / (z^2 - 2 z cos(k) + 1) {{AutoCat}} o66t339e5dygtgv15w3ywygxhj1cjz0 772667 772664 2026-09-21T13:14:50Z Xhungab 23827 772667 wikitext text/x-wiki __NOTOC__ [[Catégorie:Mathc initiation (livre)]] [[Mathc initiation/a582#Transformées en Z usuelles, des signaux causales discrets :| Sommaire]] ==La fonction cosinus discret : g(n) = cos(kn)== '''La transformée en Z de g[n] est : G[z] = [z^2 - z cos(k)] / [z^2 - 2 z cos(k) + 1]''' g[n] = cos(kn) * cos(kn) = (e^(kni)+e^(-kni))/2 g[n] = (e^(kni) + e^(-kni))/2 g[n] = 1/2 ('''e^(kni)''' + e^(-kni)) f(n) = a^n F[z] = z/(z-a) G[z] = 1/2 '''(z/(z-e^(ki))''' + z/(z-e^(-ki)) f(n) = '''e^(kni)''' F[z] = '''z/(z-e^(ki))''' G[z] = '''z'''/2 (1/(z-e^(ki)) + 1/(z-e^(-ki)) Mis au même dénominateur. G[z] = z/2 (('''z'''-e^(-ki) + ('''z'''-e^(ki)) / ((z-e^(ki))(z-e^(-ki))) G[z] = z/2 ((2z-('''e^(-ki)+e^(ki)''')) / ((z-e^(ki))(z-e^(-ki))) '''Numérateur :''' G[z] = z/2 ((2z-('''e^(-ki)+e^(ki))''') / ... * '''e^(ki)+e^(-ki) = 2 cos(k)''' G[z] = z/2 ((2z - 2 cos(k)) /... G[z] = z (( z - cos(k)) /... G[z] = '''( z^2- z cos(k))''' /... '''Dénominateur :''' G[z] = ... / ( (z - e^(ki)) (z - e^(-ki)) ) Développons : G[z] = ... / ( (z^2 - z e^(-ki)-z e^(ki) + '''e^(ki)(e^(-ki)''') G[z] = ... / ( (z^2 - z e^(-ki)-z e^(ki) + 1) '''e^(ki)(e^(-ki) = 1''' G[z] = ... / ( (z^2 - z '''(e^(-ki)+e^(ki))''' + 1) * '''e^(ki)+e^(-ki) = 2 cos(k)''' G[z] = ... / ( (z^2 - z ('''2 cos(k)''') + 1) G[z] = ... / ('''z^2 - 2 z cos(k) + 1)''' '''Donc :''' G[z] = (z^2 - z cos(k) ) / (z^2 - 2 z cos(k) + 1) {{AutoCat}} 31yicd9jjzb3c8qimg68i583akn34e1 Mathc initiation/0072 0 84518 772666 2026-09-21T13:13:32Z Xhungab 23827 news 772666 wikitext text/x-wiki __NOTOC__ [[Catégorie:Mathc initiation (livre)]] [[Mathc initiation/a582#Transformées en Z usuelles, des signaux causales discrets :| Sommaire]] ==La fonction sinus discret : h(n) = sin(kn)== '''La transformée en Z de h[n] est :''' H[z] = z sin(k)]/[z^2-2z cos(k)+1 h[n] = sin(kn) * sin(kn) = (e^(kni)-e^(-kni))/2i h[n] = (e^(kni)-e^(-kni))/2i h[n] = 1/2i ('''e^(kni)'''-e^(-kni)) f(n) = a^n F[z] = z/(z-a) H[z] = 1/2i (z/(z-e^(ki)) - z/(z-e^(-ki)) f(n) = '''e^(kni)''' F[z] = '''z/(z-e^(ki))''' H[z] = z/2i (1/(z-e^(ki)) - 1/(z-e^(-ki)) Mis au même dénominateur. H[z] = z/2i ((z-e^(-ki) - (z-e^(ki)) / ((z-e^(ki))(z-e^(-ki))) H[z] = z/2i ( z-e^(-ki) - z +e^(ki)) / ((z-e^(ki))(z-e^(-ki))) H[z] = z/2i ((-e^(-ki) +e^(ki)) / ((z-e^(ki))(z-e^(-ki))) '''Numérateur :''' H[z] = z/2i ((-e^(-ki)+e^(ki)) / ... * e^(ki)-e^(-ki) = 2i sin(k) H[z] = z/2i (2i sin(k)) /... H[z] = '''z sin(k)''' /... '''Dénominateur :''' G[z] = ... / ( (z - e^(ki)) (z - e^(-ki)) ) Développons : G[z] = ... / ( (z^2 - z e^(-ki)-z e^(ki) + '''e^(ki)(e^(-ki)''') G[z] = ... / ( (z^2 - z e^(-ki)-z e^(ki) + 1) '''e^(ki)(e^(-ki) = 1''' G[z] = ... / ( (z^2 - z '''(e^(-ki)+e^(ki))''' + 1) '''e^(ki)+e^(-ki) = 2 cos(k)''' G[z] = ... / ( (z^2 - z ('''2 cos(k)''') + 1) G[z] = ... / ('''z^2 - 2 z cos(k) + 1)''' '''Donc :''' H[z] = z sin(k) / (z^2 - 2 z cos(k) + 1) {{AutoCat}} e39nrwu6vio5sywhtj8su5nkor9g9sw Mathc initiation/0073 0 84519 772813 2026-09-22T11:51:02Z Xhungab 23827 news 772813 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] * z^2 <--> δ[n+2] * z^k <--> δ[n+k] * 1 <--> δ[n] * 5 <--> 5 δ[n] * 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 {{AutoCat}} 5bse711bu4g66rtz1m94iaw1fzjgl6n 772814 772813 2026-09-22T11:53:07Z Xhungab 23827 772814 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 {{AutoCat}} tmwx3dznmdyjzyn11i2aygdqbgvlqtm